feat: 更换转视频的带提示输入框
This commit is contained in:
9
package-lock.json
generated
9
package-lock.json
generated
@@ -35,7 +35,8 @@
|
||||
"vue-i18n": "^9.6.1",
|
||||
"vue-router": "^4.0.3",
|
||||
"vuedraggable": "^4.1.0",
|
||||
"vuex": "^4.0.0"
|
||||
"vuex": "^4.0.0",
|
||||
"x-sender": "^1.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/three": "^0.174.0",
|
||||
@@ -10106,6 +10107,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/x-sender": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/x-sender/-/x-sender-1.1.6.tgz",
|
||||
"integrity": "sha512-es24YnTY1+g3TdDVrEgRVW8uW2nYPyHjQveBgZxk8JrB7809yd8AkYptrLgqL1trpUZtMILVW+2GIoB0V5HfVQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/xml-name-validator": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz",
|
||||
|
||||
@@ -41,7 +41,8 @@
|
||||
"vue-i18n": "^9.6.1",
|
||||
"vue-router": "^4.0.3",
|
||||
"vuedraggable": "^4.1.0",
|
||||
"vuex": "^4.0.0"
|
||||
"vuex": "^4.0.0",
|
||||
"x-sender": "^1.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/three": "^0.174.0",
|
||||
@@ -105,4 +106,4 @@
|
||||
"not dead",
|
||||
"not ie 11"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -687,6 +687,7 @@ export default defineComponent({
|
||||
return params
|
||||
}
|
||||
let getPrductimg = async () => {
|
||||
|
||||
let obj = getData()
|
||||
// let imageStrength = productimg.productimgSimilarity == 100? 95 :productimg.productimgSimilarity
|
||||
let imageStrength = (70 / 100) * productimg.productimgSimilarity
|
||||
@@ -1233,11 +1234,8 @@ export default defineComponent({
|
||||
const showProductList = ref(false)
|
||||
const productType = ref('first')
|
||||
const fullProductImages = computed(() => {
|
||||
|
||||
return productimg.likeDesignCollectionList.flatMap(item =>
|
||||
(item.childList || []).filter(
|
||||
child => child.resultType !== 'PoseTransfer'
|
||||
)
|
||||
(item.childList || []).filter(child => child.resultType !== 'PoseTransfer')
|
||||
)
|
||||
})
|
||||
const handleOpenProduct = (type: 'first' | 'last') => {
|
||||
|
||||
55
src/component/home/tools/poseTransfer/Sender.vue
Normal file
55
src/component/home/tools/poseTransfer/Sender.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<div id="sender" ref="senderRef"></div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onBeforeMount } from 'vue'
|
||||
import XSender, { Options } from 'x-sender'
|
||||
import 'x-sender/style'
|
||||
|
||||
const props = defineProps({
|
||||
nodeList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
|
||||
const senderRef = ref<HTMLElement | null>(null)
|
||||
let sender: XSender | null = null
|
||||
const options: Options = {
|
||||
placeholder: '请输入内容'
|
||||
}
|
||||
|
||||
const chatNode = [[]]
|
||||
|
||||
const createSender = () => {
|
||||
if (sender) {
|
||||
sender.destroy()
|
||||
}
|
||||
sender = new XSender(senderRef.value!, options)
|
||||
sender.reset({
|
||||
clearHistory: true,
|
||||
chatNode: [props.nodeList]
|
||||
})
|
||||
}
|
||||
|
||||
const getContext = () => {
|
||||
const text = sender.getText()
|
||||
return text
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
createSender()
|
||||
})
|
||||
|
||||
onBeforeMount(() => {
|
||||
if (sender) {
|
||||
sender.destroy()
|
||||
sender = null
|
||||
}
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
getContext
|
||||
})
|
||||
</script>
|
||||
<style lang="less" scoped></style>
|
||||
@@ -168,15 +168,22 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="prompt-input-container" v-show="!showMotion">
|
||||
<div class="title" style="display: flex; gap: .5rem;">
|
||||
{{ $t('ProductImg.Prompt') }}
|
||||
<a :href="
|
||||
locale == 'CHINESE_SIMPLIFIED'?
|
||||
'https://aida-user-manual-chinese.super.site/2b08f755cedd80a985cffdf2af80c538':
|
||||
'https://aida-user-manual.super.site/advanced-tool/animated-product-image/to-product-video-prompt-assist '" target="_blank">
|
||||
<i class="fi fi-rs-interrogation tips-icon" style="font-size: 1.8rem; color: #000;"/>
|
||||
</a>
|
||||
</div>
|
||||
<div class="title" style="display: flex; gap: 0.5rem">
|
||||
{{ $t('ProductImg.Prompt') }}
|
||||
<a
|
||||
:href="
|
||||
locale == 'CHINESE_SIMPLIFIED'
|
||||
? 'https://aida-user-manual-chinese.super.site/2b08f755cedd80a985cffdf2af80c538'
|
||||
: 'https://aida-user-manual.super.site/advanced-tool/animated-product-image/to-product-video-prompt-assist '
|
||||
"
|
||||
target="_blank"
|
||||
>
|
||||
<i
|
||||
class="fi fi-rs-interrogation tips-icon"
|
||||
style="font-size: 1.8rem; color: #000"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<!-- <div class="title">
|
||||
<span>{{ $t('ProductImg.Prompt') }}</span>
|
||||
<i class="fi fi-rs-interrogation tips-icon" />
|
||||
@@ -334,7 +341,7 @@ export default defineComponent({
|
||||
generalDrag,
|
||||
promptInput,
|
||||
Tips,
|
||||
Product
|
||||
Product,
|
||||
},
|
||||
props: {
|
||||
isDesignPage: {
|
||||
@@ -1539,11 +1546,11 @@ export default defineComponent({
|
||||
}
|
||||
.prompt-input-container {
|
||||
margin-top: 4rem;
|
||||
.title{
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 1rem;
|
||||
i{
|
||||
i {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -1,30 +1,67 @@
|
||||
export const getFirstAndLastFrame = (t: (key: string) => string) => [
|
||||
{ id: '1', type: 'text', value: t('poseTransfer.firstAndLastFrameText1') },
|
||||
{ id: '1', type: 'Write', text: t('poseTransfer.firstAndLastFrameText1') },
|
||||
{
|
||||
id: '2',
|
||||
type: 'input',
|
||||
value: '',
|
||||
type: 'Input',
|
||||
text: '',
|
||||
key: 'content1',
|
||||
placeholder: t('poseTransfer.firstAndLastFramePlaceholder1')
|
||||
},
|
||||
{ id: '3', type: 'text', value: t('poseTransfer.firstAndLastFrameText2') },
|
||||
{ id: '4', type: 'input', value: '', placeholder: t('poseTransfer.firstAndLastFramePlaceholder2') },
|
||||
{ id: '3', type: 'Write', text: t('poseTransfer.firstAndLastFrameText2') },
|
||||
{
|
||||
id: '4',
|
||||
type: 'Input',
|
||||
text: '',
|
||||
key: 'content2',
|
||||
placeholder: t('poseTransfer.firstAndLastFramePlaceholder2')
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
type: 'text',
|
||||
value: t('poseTransfer.firstAndLastFrameText3')
|
||||
type: 'Write',
|
||||
text: t('poseTransfer.firstAndLastFrameText3')
|
||||
}
|
||||
]
|
||||
|
||||
export const getFirstFrame = (t: (key: string) => string) => [
|
||||
{ id: '1', type: 'text', value: t('poseTransfer.firstFrameText1') },
|
||||
{ id: '2', type: 'input', value: '', placeholder: t('poseTransfer.firstFramePlaceholder1') },
|
||||
{ id: '3', type: 'text', value: t('poseTransfer.firstFrameText2') },
|
||||
{ id: '4', type: 'input', value: '', placeholder: t('poseTransfer.firstFramePlaceholder2') },
|
||||
{ id: '5', type: 'text', value: t('poseTransfer.firstFrameText3') },
|
||||
{ id: '6', type: 'input', value: '', placeholder: t('poseTransfer.firstFramePlaceholder3') },
|
||||
{ id: '7', type: 'text', value: t('poseTransfer.firstFrameText4') },
|
||||
{ id: '8', type: 'input', value: '', placeholder: t('poseTransfer.firstFramePlaceholder4') },
|
||||
{ id: '9', type: 'text', value: t('poseTransfer.firstFrameText5') },
|
||||
{ id: '10', type: 'input', value: '', placeholder: t('poseTransfer.firstFramePlaceholder5') },
|
||||
{ id: '11', type: 'text', value: t('poseTransfer.firstFrameText6') }
|
||||
{ id: '1', type: 'Write', text: t('poseTransfer.firstFrameText1') },
|
||||
{
|
||||
id: '2',
|
||||
type: 'Input',
|
||||
text: '',
|
||||
key: 'content1',
|
||||
placeholder: t('poseTransfer.firstFramePlaceholder1')
|
||||
},
|
||||
{ id: '3', type: 'Write', text: t('poseTransfer.firstFrameText2') },
|
||||
{
|
||||
id: '4',
|
||||
type: 'Input',
|
||||
text: '',
|
||||
key: 'content2',
|
||||
placeholder: t('poseTransfer.firstFramePlaceholder2')
|
||||
},
|
||||
{ id: '5', type: 'Write', text: t('poseTransfer.firstFrameText3') },
|
||||
{
|
||||
id: '6',
|
||||
type: 'Input',
|
||||
text: '',
|
||||
key: 'content3',
|
||||
placeholder: t('poseTransfer.firstFramePlaceholder3')
|
||||
},
|
||||
{ id: '7', type: 'Write', text: t('poseTransfer.firstFrameText4') },
|
||||
{
|
||||
id: '8',
|
||||
type: 'Input',
|
||||
text: '',
|
||||
key: 'content4',
|
||||
placeholder: t('poseTransfer.firstFramePlaceholder4')
|
||||
},
|
||||
{ id: '9', type: 'Write', text: t('poseTransfer.firstFrameText5') },
|
||||
{
|
||||
id: '10',
|
||||
type: 'Input',
|
||||
text: '',
|
||||
key: 'content5',
|
||||
placeholder: t('poseTransfer.firstFramePlaceholder5')
|
||||
},
|
||||
{ id: '11', type: 'Write', text: t('poseTransfer.firstFrameText6') }
|
||||
]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, toRefs, nextTick, useTemplateRef, watch } from 'vue'
|
||||
import Sender from './Sender.vue'
|
||||
|
||||
interface ContentItem {
|
||||
id: string
|
||||
@@ -15,541 +16,25 @@ const props = defineProps({
|
||||
}
|
||||
})
|
||||
|
||||
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: [{ id: '1', type: 'text', value: '' }]
|
||||
})
|
||||
|
||||
const editableArea = ref<HTMLElement>()
|
||||
const { content } = toRefs(data)
|
||||
|
||||
const cursorState = ref({
|
||||
isContainerClick: false
|
||||
})
|
||||
|
||||
// 添加中文输入状态管理
|
||||
const compositionState = reactive({
|
||||
isComposing: false,
|
||||
currentInputIndex: -1
|
||||
})
|
||||
|
||||
// 检查并删除末尾的空文本框
|
||||
const removeLastEmptyTextIfNeeded = () => {
|
||||
const lastItem = data.content[data.content.length - 1]
|
||||
if (lastItem && lastItem.type === 'text' && lastItem.value === '') {
|
||||
data.content.pop()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 确保末尾有空文本框
|
||||
const ensureEmptyTextAtEnd = () => {
|
||||
const lastItem = data.content[data.content.length - 1]
|
||||
if (!lastItem || lastItem.type !== 'text' || lastItem.value !== '') {
|
||||
const newItem: ContentItem = {
|
||||
id: Date.now().toString(),
|
||||
type: 'text',
|
||||
value: ''
|
||||
}
|
||||
data.content.push(newItem)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 元素信息获取
|
||||
const getCurrentElementInfo = () => {
|
||||
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
|
||||
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
element = (node as Text).parentElement
|
||||
} else {
|
||||
element = node as HTMLElement
|
||||
}
|
||||
|
||||
if (!element) return null
|
||||
|
||||
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'
|
||||
|
||||
const item = data.content[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 }
|
||||
}
|
||||
|
||||
//光标设置
|
||||
const setCursorToElement = (element: HTMLElement, position: 'start' | 'end') => {
|
||||
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')
|
||||
}
|
||||
|
||||
selection?.removeAllRanges()
|
||||
selection?.addRange(range)
|
||||
}
|
||||
|
||||
// 键盘事件处理
|
||||
const handleKeydown = (event: KeyboardEvent) => {
|
||||
const elementInfo = getCurrentElementInfo()
|
||||
if (!elementInfo) return
|
||||
|
||||
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
|
||||
|
||||
case 'ArrowLeft':
|
||||
if (isAtStart && index > 0) {
|
||||
event.preventDefault()
|
||||
navigateToElement(index - 1, 'end')
|
||||
}
|
||||
break
|
||||
|
||||
case 'ArrowRight':
|
||||
if (isAtEnd && index < data.content.length - 1) {
|
||||
event.preventDefault()
|
||||
navigateToElement(index + 1, 'start')
|
||||
} else if (isAtEnd && index === data.content.length - 1) {
|
||||
// 在最后一个元素末尾按右箭头,确保有一个空文本框
|
||||
ensureEmptyTextAtEnd()
|
||||
nextTick(() => {
|
||||
navigateToElement(index + 1, 'start')
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 跨元素删除逻辑
|
||||
const handleCrossElementDelete = (currentIndex: number) => {
|
||||
const prevIndex = currentIndex - 1
|
||||
const prevItem = data.content[prevIndex]
|
||||
|
||||
if (prevItem.type === 'input') {
|
||||
if (prevItem.value.trim() === '') {
|
||||
// 删除空输入框
|
||||
data.content.splice(prevIndex, 1)
|
||||
nextTick(() => {
|
||||
// 删除输入框后,先删除末尾的空文本框
|
||||
removeLastEmptyTextIfNeeded()
|
||||
// 然后聚焦到正确的位置
|
||||
if (prevIndex < data.content.length) {
|
||||
focusElement(prevIndex, 'end')
|
||||
} else if (data.content.length > 0) {
|
||||
focusElement(data.content.length - 1, 'end')
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// 删除输入框最后一个字符,但保留输入框
|
||||
const newValue = prevItem.value.slice(0, -1)
|
||||
data.content[prevIndex].value = newValue
|
||||
updateInputDisplay(prevIndex)
|
||||
nextTick(() => focusElement(prevIndex, 'end'))
|
||||
}
|
||||
} else {
|
||||
// 文本框:移动到前一个文本框末尾,让浏览器正常删除
|
||||
// 先删除末尾的空文本框
|
||||
removeLastEmptyTextIfNeeded()
|
||||
focusElement(prevIndex, 'end')
|
||||
}
|
||||
}
|
||||
|
||||
// 导航到元素
|
||||
const navigateToElement = (targetIndex: number, position: 'start' | 'end') => {
|
||||
const targetItem = data.content[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)
|
||||
}
|
||||
|
||||
// 焦点设置
|
||||
const focusElement = (index: number, position: 'start' | 'end') => {
|
||||
const item = data.content[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)
|
||||
}
|
||||
|
||||
// 输入框显示管理
|
||||
const updateInputDisplay = (index: number) => {
|
||||
const item = data.content[index]
|
||||
if (item.type !== 'input') return
|
||||
|
||||
const inputElement = editableArea.value?.querySelector(
|
||||
`.input-field[data-index="${index}"] .input-content`
|
||||
) as HTMLElement
|
||||
|
||||
if (!inputElement) return
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
const handleInputChange = (index: number, event: Event) => {
|
||||
// 如果是中文输入过程中,不处理
|
||||
if (compositionState.isComposing && compositionState.currentInputIndex === index) {
|
||||
return
|
||||
}
|
||||
|
||||
const target = event.target as HTMLSpanElement
|
||||
const item = data.content[index]
|
||||
|
||||
// 如果当前显示placeholder,但内容已改变(比如通过粘贴),清除placeholder
|
||||
if (
|
||||
target.classList.contains('has-placeholder') &&
|
||||
target.textContent !== item.placeholder
|
||||
) {
|
||||
target.classList.remove('has-placeholder')
|
||||
const newValue = target.textContent || ''
|
||||
data.content[index].value = newValue
|
||||
|
||||
// 如果粘贴后内容为空,重新显示 placeholder
|
||||
if (newValue.trim() === '' && item.placeholder) {
|
||||
target.classList.add('has-placeholder')
|
||||
target.textContent = item.placeholder
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 正常情况下的处理
|
||||
if (!target.classList.contains('has-placeholder')) {
|
||||
const newValue = target.textContent || ''
|
||||
data.content[index].value = newValue
|
||||
|
||||
// 如果内容变空,显示placeholder
|
||||
if (newValue.trim() === '' && item.placeholder) {
|
||||
target.classList.add('has-placeholder')
|
||||
target.textContent = item.placeholder
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 增强中文输入事件处理
|
||||
const handleCompositionStart = (index: number, event: CompositionEvent) => {
|
||||
compositionState.isComposing = true
|
||||
compositionState.currentInputIndex = index
|
||||
|
||||
const target = event.target as HTMLSpanElement
|
||||
// 如果是placeholder状态,开始中文输入时清除placeholder
|
||||
if (target.classList.contains('has-placeholder')) {
|
||||
target.classList.remove('has-placeholder')
|
||||
target.textContent = ''
|
||||
data.content[index].value = ''
|
||||
}
|
||||
}
|
||||
|
||||
// 添加专门的粘贴事件处理
|
||||
const handleInputPaste = (event: ClipboardEvent, index: number) => {
|
||||
const target = event.target as HTMLSpanElement
|
||||
const item = data.content[index]
|
||||
|
||||
// 如果当前显示 placeholder,先清除它
|
||||
if (target.classList.contains('has-placeholder')) {
|
||||
event.preventDefault()
|
||||
target.classList.remove('has-placeholder')
|
||||
target.textContent = ''
|
||||
|
||||
// 获取粘贴的内容
|
||||
const pasteData = event.clipboardData?.getData('text') || ''
|
||||
document.execCommand('insertText', false, pasteData)
|
||||
|
||||
// 更新值
|
||||
const newValue = target.textContent || ''
|
||||
data.content[index].value = newValue
|
||||
|
||||
// 如果粘贴后内容为空,重新显示 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
|
||||
|
||||
// 如果是中文输入过程中,不处理普通按键
|
||||
if (compositionState.isComposing) {
|
||||
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
|
||||
|
||||
// 如果光标在输入框开头,阻止默认行为,让外部的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')
|
||||
data.content[index].value = event.key
|
||||
|
||||
// 移动光标到末尾
|
||||
nextTick(() => {
|
||||
setCursorToElement(target, 'end')
|
||||
})
|
||||
}
|
||||
}
|
||||
// 添加对 Ctrl+V 的处理
|
||||
else if ((event.ctrlKey || event.metaKey) && event.key === 'v') {
|
||||
// 延迟处理,等待粘贴内容实际插入
|
||||
setTimeout(() => {
|
||||
handlePasteInInput(index, target)
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
|
||||
const handlePasteInInput = (index: number, element: HTMLElement) => {
|
||||
const item = data.content[index]
|
||||
|
||||
// 如果当前显示 placeholder,清除它
|
||||
if (element.classList.contains('has-placeholder')) {
|
||||
element.classList.remove('has-placeholder')
|
||||
// 获取粘贴后的实际内容
|
||||
const newValue = element.textContent || ''
|
||||
data.content[index].value = newValue
|
||||
|
||||
// 如果粘贴后内容为空,重新显示 placeholder
|
||||
if (newValue.trim() === '' && item.placeholder) {
|
||||
element.classList.add('has-placeholder')
|
||||
element.textContent = item.placeholder
|
||||
}
|
||||
} else {
|
||||
// 正常情况,直接更新值
|
||||
const newValue = element.textContent || ''
|
||||
data.content[index].value = newValue
|
||||
|
||||
// 检查是否需要显示 placeholder
|
||||
if (newValue.trim() === '' && item.placeholder) {
|
||||
element.classList.add('has-placeholder')
|
||||
element.textContent = item.placeholder
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleCompositionEnd = (index: number, event: CompositionEvent) => {
|
||||
// 延迟设置 isComposing 为 false,确保所有相关事件都处理完毕
|
||||
setTimeout(() => {
|
||||
compositionState.isComposing = false
|
||||
compositionState.currentInputIndex = -1
|
||||
}, 0)
|
||||
|
||||
const target = event.target as HTMLSpanElement
|
||||
const newValue = target.textContent || ''
|
||||
data.content[index].value = newValue
|
||||
|
||||
// 如果中文输入后内容为空,显示placeholder
|
||||
const item = data.content[index]
|
||||
if (newValue.trim() === '' && item.placeholder) {
|
||||
// 延迟显示 placeholder,确保光标位置正确
|
||||
nextTick(() => {
|
||||
if (target.textContent?.trim() === '') {
|
||||
target.classList.add('has-placeholder')
|
||||
target.textContent = item.placeholder
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 添加输入框焦点事件处理,确保中文输入状态正确重置
|
||||
const handleInputFocus = (index: number) => {
|
||||
compositionState.currentInputIndex = index
|
||||
}
|
||||
|
||||
// 修改输入框的 blur 处理
|
||||
const handleInputBlur = (index: number) => {
|
||||
// 延迟重置状态,避免与 compositionend 冲突
|
||||
setTimeout(() => {
|
||||
compositionState.isComposing = false
|
||||
compositionState.currentInputIndex = -1
|
||||
}, 100)
|
||||
updateInputDisplay(index)
|
||||
}
|
||||
|
||||
// 容器点击处理
|
||||
const handleContainerClick = (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement
|
||||
|
||||
if (target === editableArea.value) {
|
||||
event.preventDefault()
|
||||
cursorState.value.isContainerClick = true
|
||||
|
||||
// 确保末尾有空文本框并聚焦到它
|
||||
ensureEmptyTextAtEnd()
|
||||
nextTick(() => {
|
||||
focusElement(data.content.length - 1, 'start')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
const initPlaceholders = () => {
|
||||
nextTick(() => {
|
||||
data.content.forEach((_, index) => updateInputDisplay(index))
|
||||
// 确保初始状态下有一个空文本框
|
||||
ensureEmptyTextAtEnd()
|
||||
})
|
||||
}
|
||||
|
||||
const getFullText = () => {
|
||||
if (assistModel.value) {
|
||||
return data.content
|
||||
.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 senderRef.value?.getContext().replaceAll('[', '').replaceAll(']', '')
|
||||
}
|
||||
return textareaValue.value
|
||||
}
|
||||
|
||||
const textareaValue = ref('')
|
||||
const assistModel = ref(false)
|
||||
const syncContentFromProps = (
|
||||
newContent: ContentItem[] = props.content as ContentItem[]
|
||||
) => {
|
||||
data.content = JSON.parse(JSON.stringify(newContent || []))
|
||||
if (assistModel.value) {
|
||||
initPlaceholders()
|
||||
}
|
||||
}
|
||||
|
||||
const senderRef = useTemplateRef<HTMLDivElement>('senderRef')
|
||||
const handleClickAssistBtn = () => {
|
||||
assistModel.value = !assistModel.value
|
||||
if (assistModel.value) {
|
||||
syncContentFromProps()
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.content,
|
||||
newVal => {
|
||||
syncContentFromProps(newVal as ContentItem[])
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
const textareaRef = useTemplateRef<HTMLTextAreaElement>('textareaRef')
|
||||
|
||||
defineExpose({
|
||||
getFullText,
|
||||
content
|
||||
getFullText
|
||||
// content
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -566,39 +51,9 @@ defineExpose({
|
||||
<span>{{ $t('ProductImg.PromptAssit') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- {{ data.content }} -->
|
||||
<div
|
||||
v-show="assistModel"
|
||||
ref="editableArea"
|
||||
class="promptInput"
|
||||
@keydown="handleKeydown"
|
||||
@click="handleContainerClick"
|
||||
>
|
||||
<div class="promptinput-wrapper">
|
||||
<template v-for="(item, index) in data.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)"
|
||||
@focus="() => handleInputFocus(index)"
|
||||
@blur="() => handleInputBlur(index)"
|
||||
@paste="e => handleInputPaste(e, index)"
|
||||
@compositionstart="e => handleCompositionStart(index, e)"
|
||||
@compositionend="e => handleCompositionEnd(index, e)"
|
||||
></span>
|
||||
</span>
|
||||
</template>
|
||||
<div v-show="assistModel" class="sender-container">
|
||||
<div class="sender-wrapper">
|
||||
<Sender ref="senderRef" :nodeList="content" />
|
||||
</div>
|
||||
<div class="asistant-btn" @click="handleClickAssistBtn">
|
||||
<i class="fi fi-bs-magic-wand asistant-icon"></i>
|
||||
@@ -627,38 +82,18 @@ defineExpose({
|
||||
position: relative;
|
||||
padding-bottom: 4rem;
|
||||
box-sizing: content-box;
|
||||
|
||||
.promptinput-wrapper {
|
||||
}
|
||||
.sender-container {
|
||||
position: relative;
|
||||
border-radius: 10px;
|
||||
border: 2px solid #dcdfe6;
|
||||
padding: 1.5rem 1.5rem 3rem;
|
||||
font-size: 1.8rem;
|
||||
.sender-wrapper {
|
||||
min-height: 12rem;
|
||||
max-height: 14rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.text-field {
|
||||
display: inline;
|
||||
outline: none;
|
||||
padding: 0.2rem 0;
|
||||
font-size: 1.8rem;
|
||||
min-width: 2px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
display: inline-block;
|
||||
margin: 0 0.2rem;
|
||||
padding: 0.2rem 1rem;
|
||||
font-size: 1.8rem;
|
||||
border-radius: 4px;
|
||||
|
||||
.input-content {
|
||||
outline: none;
|
||||
display: inline-block;
|
||||
min-width: 2rem;
|
||||
|
||||
&.has-placeholder {
|
||||
color: #b9b9b9;
|
||||
}
|
||||
}
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -367,22 +367,22 @@ export default {
|
||||
TipsStart: 'User can use ‘ ',
|
||||
TipsEnd: 'Edit Product Image ’ to generate first or last frames.',
|
||||
PormptPlaceholder: 'Enter the scene you want to describe...',
|
||||
firstAndLastFrameText1: 'As the video progresses, use',
|
||||
firstAndLastFramePlaceholder1: '[Camera Movement]',
|
||||
firstAndLastFrameText2: 'to follow the motion, under',
|
||||
firstAndLastFramePlaceholder2: '[Light]',
|
||||
firstAndLastFrameText1: 'As the video progresses, use ',
|
||||
firstAndLastFramePlaceholder1: 'Camera Movement',
|
||||
firstAndLastFrameText2: ' to follow the motion, under',
|
||||
firstAndLastFramePlaceholder2: 'Light',
|
||||
firstAndLastFrameText3:
|
||||
', maintaining full consistency of model identity, styling, and outfit across all frames.',
|
||||
firstFrameText1: 'Set the',
|
||||
firstFramePlaceholder1: '[Scene]',
|
||||
firstFrameText2: ', where the model',
|
||||
firstFramePlaceholder2: '[Motion]',
|
||||
firstFrameText3: 'use a',
|
||||
firstFramePlaceholder3: '[Camera Movement]',
|
||||
firstFrameText4: 'combined with a',
|
||||
firstFramePlaceholder4: '[Camera Movement]',
|
||||
firstFrameText5: ', under',
|
||||
firstFramePlaceholder5: '[Light]',
|
||||
' , maintaining full consistency of model identity, styling, and outfit across all frames.',
|
||||
firstFrameText1: 'Set the ',
|
||||
firstFramePlaceholder1: 'Scene',
|
||||
firstFrameText2: ', where the model ',
|
||||
firstFramePlaceholder2: 'Motion',
|
||||
firstFrameText3: ' use a ',
|
||||
firstFramePlaceholder3: 'Camera Movement',
|
||||
firstFrameText4: ' combined with a ',
|
||||
firstFramePlaceholder4: 'Camera Movement',
|
||||
firstFrameText5: ', under ',
|
||||
firstFramePlaceholder5: 'Light',
|
||||
firstFrameText6: ', complementing the look.'
|
||||
},
|
||||
LibraryPage: {
|
||||
|
||||
Reference in New Issue
Block a user