1417 lines
34 KiB
Vue
1417 lines
34 KiB
Vue
<template>
|
||
<div class="assist-input-wrapper flex flex-col" :class="{ agent: isAgentMode }">
|
||
<div class="animate-container flex-1 flex flex-col">
|
||
<div class="scroll-content flex-col">
|
||
<div v-if="uploadedImages.length > 0" class="image-preview-list flex wrap">
|
||
<div
|
||
v-for="(image, index) in uploadedImages"
|
||
:key="index"
|
||
class="image-preview-item"
|
||
>
|
||
<img
|
||
:src="image.url"
|
||
:alt="image.name"
|
||
class="preview-image"
|
||
@click="previewImage(image.url)"
|
||
/>
|
||
<div class="image-remove-btn" @click="removeImage(index)">
|
||
<SvgIcon name="delete" size="16" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="editor-wrapper">
|
||
<div v-if="showPlaceholder" class="editor-placeholder">
|
||
{{ $t('Input.placeholder') }}
|
||
</div>
|
||
<div
|
||
ref="editorRef"
|
||
class="editor"
|
||
contenteditable="true"
|
||
@input="handleEditorInput"
|
||
@paste="handleEditorPaste"
|
||
@keydown="handleKeyDown"
|
||
></div>
|
||
</div>
|
||
</div>
|
||
<div class="operate flex align-center space-between">
|
||
<div class="left flex align-center">
|
||
<div class="agent-operate flex flex-center">
|
||
<el-popover
|
||
placement="top"
|
||
trigger="click"
|
||
popper-class="agent-plus-popover"
|
||
>
|
||
<template #reference>
|
||
<SvgIcon name="plus" color="#0D0D0D" size="16" />
|
||
</template>
|
||
<template #default>
|
||
<div class="agent-modal flex flex-col">
|
||
<div class="file flex align-center" @click="triggerFileUpload">
|
||
<img src="@/assets/icons/attach.svg" class="file-icon" />
|
||
<span>Upload files</span>
|
||
</div>
|
||
<div class="gap"></div>
|
||
<div class="report flex align-center" @click="toogltReportTag">
|
||
<SvgIcon color="#5A5A5A" name="light" size="11" />
|
||
<span>Trending report</span>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</el-popover>
|
||
</div>
|
||
<div
|
||
class="attach flex flex-center"
|
||
@click="triggerFileUpload"
|
||
v-if="!isAgentMode"
|
||
>
|
||
<img src="@/assets/icons/attach.svg" />
|
||
</div>
|
||
<input
|
||
ref="fileInputRef"
|
||
type="file"
|
||
accept="image/*"
|
||
style="display: none"
|
||
@change="handleFileChange"
|
||
/>
|
||
<el-select
|
||
v-if="!isAgentMode"
|
||
v-model="typeValue"
|
||
:placeholder="$t('Input.typePlaceholder')"
|
||
>
|
||
<el-option
|
||
v-for="item in typeOptions"
|
||
class="input-option"
|
||
:key="item.value"
|
||
:label="$t(item.label)"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
<el-select
|
||
v-if="!isAgentMode"
|
||
v-model="areaValue"
|
||
:placeholder="$t('Input.areaPlaceholder')"
|
||
>
|
||
<el-option
|
||
v-for="item in areaOptions"
|
||
class="input-option"
|
||
:key="item.value"
|
||
:label="$t(item.label)"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
<div v-if="!isAgentMode" class="fida-style-select-wrapper">
|
||
<el-select
|
||
v-model="styleValue"
|
||
:placeholder="$t('Input.stylePlaceholder')"
|
||
@focus="openStylePopup"
|
||
/>
|
||
|
||
<el-popover
|
||
v-model:visible="stylePopupVisible"
|
||
placement="top"
|
||
:width="342"
|
||
:show-arrow="false"
|
||
trigger="click"
|
||
popper-class="fida-style-select-popover"
|
||
>
|
||
<template #reference>
|
||
<div class="fida-style-select-trigger"></div>
|
||
</template>
|
||
<div class="fida-style-popover-content flex flex-col">
|
||
<div class="fida-style-popover-header">
|
||
{{ $t('Input.chooseStyle') }}
|
||
</div>
|
||
<div class="fida-style-popover-grid">
|
||
<div
|
||
v-for="item in styleOptions"
|
||
:key="item.value"
|
||
class="fida-style-popover-item flex flex-center"
|
||
:class="{ 'is-selected': tempSelectedValue === item.value }"
|
||
@click="selectStyle(item.value)"
|
||
>
|
||
<img
|
||
:src="getStyleImage(typeValue, item.value)"
|
||
class="style-bg"
|
||
/>
|
||
<span class="fida-option-label flex flex-center">{{
|
||
item.label
|
||
}}</span>
|
||
<img
|
||
v-show="tempSelectedValue === item.value"
|
||
src="@/assets/images/checked.png"
|
||
class="checked-item-icon"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div class="fida-style-popover-footer flex flex-center">
|
||
<button class="fida-confirm-btn" @click="confirmStyle">
|
||
{{ $t('Input.confirm') }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</el-popover>
|
||
</div>
|
||
<el-popover
|
||
v-model:visible="settingPopupVisible"
|
||
placement="top"
|
||
:width="342"
|
||
:show-arrow="false"
|
||
trigger="click"
|
||
popper-class="fida-setting-popover"
|
||
>
|
||
<template #reference>
|
||
<img src="@/assets/images/setting.png" class="setting-icon" />
|
||
</template>
|
||
<div class="fida-setting-popover-content flex flex-col">
|
||
<div class="fida-setting-popover-header">
|
||
{{ $t('Input.styleTitle') }}
|
||
</div>
|
||
<div class="fida-setting-slider-list">
|
||
<div
|
||
v-for="item in settingOptions"
|
||
:key="item.label"
|
||
class="fida-setting-slider-item"
|
||
>
|
||
<div class="fida-slider-label">{{ $t(item.label) }}</div>
|
||
<div class="fida-slider-row flex align-center">
|
||
<el-slider
|
||
class="setting-popover-slider"
|
||
v-model="item.value"
|
||
:show-tooltip="false"
|
||
/>
|
||
<span class="fida-slider-value">{{ item.value }}%</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</el-popover>
|
||
</div>
|
||
<div class="right">
|
||
<div
|
||
class="create-btn flex flex-center"
|
||
v-if="!isAgentMode"
|
||
@click="handleCreateProject"
|
||
>
|
||
<img src="@/assets/images/shining.png" class="shining-icon" alt="" />
|
||
<span class="create-btn-text">{{ $t('Input.createProject') }}</span>
|
||
</div>
|
||
|
||
<div v-else class="sender-btn flex flex-center" @click="handleSendAgent">
|
||
<img
|
||
v-show="!generating"
|
||
src="@/assets/images/sender.png"
|
||
alt=""
|
||
class="sender-icon"
|
||
/>
|
||
<div v-show="generating" class="sender-pause" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
v-if="!isAgentMode"
|
||
class="report-btn flex space-between align-center"
|
||
@click="toogltReportTag"
|
||
>
|
||
<SvgIcon class="light-icon" color="#FFDB56" name="light" size="16" />
|
||
<span>{{ $t('Input.trendingReport') }}</span>
|
||
</div>
|
||
<Preview v-model="showPreview" :url="previewUrl" />
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed, ref, watch, nextTick, onMounted } from 'vue'
|
||
import { areaList } from '@/utils/area'
|
||
import { useI18n } from 'vue-i18n'
|
||
import { useRouter } from 'vue-router'
|
||
import { useAgentStore, useProjectStore } from '@/stores'
|
||
import lightIcon from '@/assets/images/light-icon.png'
|
||
import closeIcon from '@/assets/images/close-icon.png'
|
||
import restoreIcon from '@/assets/images/restore.png'
|
||
import restoreCloseIcon from '@/assets/images/tag-close.png'
|
||
import { createProject } from '@/api/agent'
|
||
import { getStyleImage } from './style'
|
||
import { uploadImage } from '@/api/upload'
|
||
import MyEvent from '@/utils/myEvent'
|
||
import Preview from '@/components/Preview/Preview.vue'
|
||
|
||
const router = useRouter()
|
||
const agentStore = useAgentStore()
|
||
const projectStore = useProjectStore()
|
||
|
||
const props = withDefaults(
|
||
defineProps<{
|
||
isAgentMode?: boolean
|
||
generating?: boolean
|
||
}>(),
|
||
{
|
||
isAgentMode: false,
|
||
generating: false
|
||
}
|
||
)
|
||
|
||
const emits = defineEmits(['send', 'pause'])
|
||
|
||
const { t } = useI18n()
|
||
|
||
// 图片上传相关
|
||
const fileInputRef = ref<HTMLInputElement | null>(null)
|
||
const uploadedImages = ref<Array<{ url: string; name: string }>>([])
|
||
|
||
// 触发文件上传
|
||
const triggerFileUpload = () => {
|
||
fileInputRef.value?.click()
|
||
}
|
||
|
||
// 处理文件选择
|
||
const handleFileChange = (event: Event) => {
|
||
const input = event.target as HTMLInputElement
|
||
|
||
if (input.files) {
|
||
Array.from(input.files).forEach((file) => {
|
||
// 只处理图片文件
|
||
if (file.type.startsWith('image/')) {
|
||
const formData = new FormData()
|
||
formData.append('file', file)
|
||
|
||
uploadImage(formData).then((res) => {
|
||
const reader = new FileReader()
|
||
reader.onload = (e) => {
|
||
uploadedImages.value.push({
|
||
url: e.target?.result as string,
|
||
name: file.name,
|
||
path: res
|
||
})
|
||
}
|
||
reader.readAsDataURL(file)
|
||
})
|
||
}
|
||
})
|
||
}
|
||
nextTick(() => {
|
||
editorRef.value?.focus()
|
||
})
|
||
input.value = ''
|
||
}
|
||
|
||
// 移除图片
|
||
const removeImage = (index: number) => {
|
||
uploadedImages.value.splice(index, 1)
|
||
}
|
||
|
||
const styleKeys: string[] = [
|
||
'Venetian Modern',
|
||
'Coastal',
|
||
'Maximalism',
|
||
'Memphis',
|
||
'Verdant',
|
||
'Century Chrome',
|
||
'Modern Revival',
|
||
'Transitional',
|
||
"Tuscan 2000's",
|
||
'Kitsch-core',
|
||
'Bauhaus',
|
||
'Constructivism',
|
||
'Nordic Noir',
|
||
'Dopamine',
|
||
'Squiggle'
|
||
]
|
||
|
||
const editorRef = ref<HTMLDivElement | null>(null)
|
||
const inputValue = ref<string>('')
|
||
const reportTags = ref([])
|
||
const customPlaceholder = ref<HTMLElement | null>(null)
|
||
|
||
// 控制占位符显示
|
||
const showPlaceholder = computed(() => {
|
||
if (!editorRef.value) return true
|
||
if (inputValue.value || reportTags.value.length > 0) return false
|
||
|
||
const editor = editorRef.value
|
||
const textContent = editor.textContent?.replace(/[\s\u200B\n]/g, '').trim() || '' // 移除空格、零宽空格、换行
|
||
const isEmptyHTML = editor.innerHTML === '' || editor.innerHTML === '<br>' // 处理浏览器 <br> 插入
|
||
const hasMeaningfulChildren = Array.from(editor.children).some(
|
||
(child) => child.tagName !== 'BR' && !child.classList.contains('editor-tag') // 排除 <br> 和 tags(如果 tags 算非空)
|
||
)
|
||
|
||
// 如果纯文本为空、无有意义子元素、且不是仅剩 <br>,则显示 placeholder
|
||
return textContent === '' && !hasMeaningfulChildren && isEmptyHTML
|
||
})
|
||
|
||
// 打字机效果显示placeholder
|
||
let typewriterTimeout: ReturnType<typeof setTimeout> | null = null
|
||
const typeWriterEffect = (element: HTMLElement, text: string, index: number = 0) => {
|
||
if (index < text.length) {
|
||
element.innerText += text.charAt(index)
|
||
typewriterTimeout = setTimeout(() => {
|
||
typeWriterEffect(element, text, index + 1)
|
||
}, 30) // 每个字符间隔30ms
|
||
}
|
||
}
|
||
|
||
// 停止打字机效果
|
||
const stopTypewriter = () => {
|
||
if (typewriterTimeout) {
|
||
clearTimeout(typewriterTimeout)
|
||
typewriterTimeout = null
|
||
}
|
||
}
|
||
|
||
// 导出给父组件调用的方法
|
||
const addReportTag = (text?: string) => {
|
||
// 使用传入的文本,如果没有传入则使用默认的翻译文本
|
||
const tagText = text || t('Input.trendingReport')
|
||
|
||
const tag = document.createElement('div')
|
||
tag.contentEditable = 'false'
|
||
const imgLeft = document.createElement('img')
|
||
const imgClose = document.createElement('img')
|
||
const textSpan = document.createElement('span')
|
||
imgClose.className = 'close-icon'
|
||
if (text) {
|
||
tag.className = 'editor-tag restore flex-center'
|
||
imgLeft.className = 'restore-icon'
|
||
imgLeft.src = restoreIcon as unknown as string
|
||
imgClose.src = restoreCloseIcon as unknown as string
|
||
imgClose.className = 'close-icon restore'
|
||
textSpan.className = 'restore-text'
|
||
} else {
|
||
tag.className = 'editor-tag report-btn flex-center'
|
||
imgLeft.className = 'light-icon'
|
||
imgLeft.src = lightIcon as unknown as string
|
||
imgClose.src = closeIcon as unknown as string
|
||
}
|
||
|
||
textSpan.innerText = tagText
|
||
|
||
imgClose.addEventListener('click', (ev) => {
|
||
ev.stopPropagation()
|
||
stopTypewriter() // 关闭标签时停止打字机效果
|
||
// remove tag when close clicked
|
||
tag.remove()
|
||
const idx = reportTags.value.indexOf(tag)
|
||
if (idx > -1) reportTags.value.splice(idx, 1)
|
||
if (customPlaceholder.value) {
|
||
customPlaceholder.value.remove()
|
||
customPlaceholder.value = null
|
||
}
|
||
})
|
||
|
||
// assemble
|
||
tag.appendChild(imgLeft)
|
||
tag.appendChild(textSpan)
|
||
tag.appendChild(imgClose)
|
||
|
||
// Insert tag at the current cursor position
|
||
const selection = window.getSelection()
|
||
|
||
// 检查selection是否在editorRef内部,不在则强制使用editorRef
|
||
const isInEditor =
|
||
editorRef.value && selection && editorRef.value.contains(selection.anchorNode)
|
||
|
||
if (isInEditor && selection && selection.rangeCount > 0) {
|
||
const range = selection.getRangeAt(0)
|
||
range.insertNode(tag)
|
||
|
||
// Insert a zero-width space text node after the tag so the caret can be placed there
|
||
const zwsp = document.createTextNode('\u200B')
|
||
if (tag.parentNode) tag.parentNode.insertBefore(zwsp, tag.nextSibling)
|
||
|
||
// Create a new collapsed range positioned inside the zwsp (after the tag)
|
||
const newRange = document.createRange()
|
||
newRange.setStart(zwsp, 1)
|
||
newRange.collapse(true)
|
||
|
||
selection.removeAllRanges()
|
||
selection.addRange(newRange)
|
||
|
||
// ensure editor has focus
|
||
editorRef.value && (editorRef.value as HTMLElement).focus()
|
||
} else if (editorRef.value) {
|
||
// If no selection in editor, append directly to editor and place caret after
|
||
editorRef.value.appendChild(tag)
|
||
const zwsp = document.createTextNode('\u200B')
|
||
editorRef.value.appendChild(zwsp)
|
||
const sel = window.getSelection()
|
||
if (sel) {
|
||
const r = document.createRange()
|
||
r.setStart(zwsp, 1)
|
||
r.collapse(true)
|
||
sel.removeAllRanges()
|
||
sel.addRange(r)
|
||
}
|
||
editorRef.value && (editorRef.value as HTMLElement).focus()
|
||
}
|
||
|
||
reportTags.value.push(tag)
|
||
const placeholderSpan = document.createElement('span')
|
||
placeholderSpan.className = 'custom-placeholder'
|
||
// 初始为空字符串,稍后通过打字机效果填充
|
||
placeholderSpan.innerText = ''
|
||
if (tag.parentNode) {
|
||
tag.parentNode.insertBefore(placeholderSpan, tag.nextSibling)
|
||
const zwsp = document.createTextNode('\u200B')
|
||
tag.parentNode.insertBefore(zwsp, placeholderSpan.nextSibling)
|
||
const newRange = document.createRange()
|
||
newRange.setStart(placeholderSpan, 0)
|
||
newRange.collapse(true)
|
||
selection.removeAllRanges()
|
||
selection.addRange(newRange)
|
||
selection.addRange(newRange)
|
||
}
|
||
customPlaceholder.value = placeholderSpan
|
||
|
||
// 打字机效果显示placeholder文本
|
||
const placeholderText =
|
||
'Generate a furniture trending report for 2026, including popular styles and design directions.'
|
||
typeWriterEffect(placeholderSpan, placeholderText)
|
||
|
||
const removePlaceholderOnInput = () => {
|
||
stopTypewriter() // 用户输入时停止打字机效果
|
||
if (placeholderSpan.parentNode) {
|
||
placeholderSpan.remove()
|
||
customPlaceholder.value = null
|
||
}
|
||
editorRef.value?.removeEventListener('input', removePlaceholderOnInput)
|
||
}
|
||
editorRef.value?.addEventListener('input', removePlaceholderOnInput)
|
||
}
|
||
|
||
const toogltReportTag = () => {
|
||
stopTypewriter() // 移除标签时停止打字机效果
|
||
// 清理掉已被删除的标签引用(从 DOM 中移除的元素)
|
||
reportTags.value = reportTags.value.filter((tag) => tag.parentNode !== null)
|
||
|
||
if (reportTags.value.length > 0) {
|
||
// 移除所有标签及其关联的零宽空格
|
||
reportTags.value.forEach((tag) => {
|
||
if (
|
||
tag.nextSibling &&
|
||
tag.nextSibling.nodeType === Node.TEXT_NODE &&
|
||
tag.nextSibling.textContent === '\u200B'
|
||
) {
|
||
tag.nextSibling.remove()
|
||
}
|
||
tag.remove()
|
||
})
|
||
reportTags.value = []
|
||
if (customPlaceholder.value) {
|
||
customPlaceholder.value.remove()
|
||
customPlaceholder.value = null
|
||
}
|
||
} else {
|
||
// 添加标签
|
||
addReportTag()
|
||
}
|
||
}
|
||
|
||
// 清理编辑器中的零宽字符
|
||
const cleanupEditor = () => {
|
||
if (!editorRef.value) return
|
||
|
||
const editor = editorRef.value
|
||
|
||
// 移除所有零宽字符
|
||
if (editor.textContent) {
|
||
const cleanedText = editor.textContent.replace(/[\u200B-\u200D\uFEFF]/g, '')
|
||
|
||
// 检查是否真的为空
|
||
if (!cleanedText.trim() && editor.children.length === 0) {
|
||
// 完全清空
|
||
editor.innerHTML = ''
|
||
editor.textContent = ''
|
||
reportTags.value = []
|
||
customPlaceholder.value = null
|
||
stopTypewriter()
|
||
}
|
||
}
|
||
}
|
||
|
||
const handleEditorInput = () => {
|
||
if (!editorRef.value) return
|
||
|
||
// 提取纯文本(排除插入的report标签)
|
||
let text = ''
|
||
const walker = document.createTreeWalker(editorRef.value, NodeFilter.SHOW_TEXT, null)
|
||
|
||
let node: Node | null
|
||
while ((node = walker.nextNode())) {
|
||
// 使用 closest() 检查当前节点的祖先元素是否包含需要排除的 class
|
||
if (node.parentElement?.closest('.custom-placeholder')) continue
|
||
if (node.parentElement?.closest('.editor-tag')) continue
|
||
text += node.textContent
|
||
}
|
||
|
||
// 移除末尾的空格和零宽空格
|
||
text = text.replace(/[\s\u200B]+$/, '')
|
||
inputValue.value = text
|
||
|
||
// 检查编辑器是否真正为空(没有可见子元素且没有文本)
|
||
const editor = editorRef.value
|
||
const hasChildElements = editor.children.length > 0
|
||
const hasTextContent = editor.textContent?.replace(/[\s\u200B]/g, '').trim().length > 0
|
||
|
||
// 如果编辑器完全为空,清空它以显示占位符
|
||
// 同时也要清空 reportTags 和 customPlaceholder
|
||
if (!hasChildElements && !hasTextContent) {
|
||
editor.innerHTML = ''
|
||
reportTags.value = []
|
||
customPlaceholder.value = null
|
||
stopTypewriter()
|
||
}
|
||
|
||
// 自动调整高度
|
||
autoResizeEditor()
|
||
}
|
||
|
||
const handleEditorPaste = (e: ClipboardEvent) => {
|
||
e.preventDefault()
|
||
const text = e.clipboardData?.getData('text/plain') || ''
|
||
document.execCommand('insertText', false, text)
|
||
}
|
||
|
||
const autoResizeEditor = () => {
|
||
const editor = editorRef.value
|
||
if (editor) {
|
||
if (props.isAgentMode) {
|
||
// editor.style.height = '6rem'
|
||
// editor.style.overflowY = 'auto'
|
||
return
|
||
} else {
|
||
// editor.style.height = 'auto'
|
||
// const maxHeight =
|
||
// 20 * parseFloat(getComputedStyle(document.documentElement).fontSize || '16')
|
||
// editor.style.height = Math.min(editor.scrollHeight, maxHeight) + 'px'
|
||
}
|
||
}
|
||
}
|
||
|
||
const handleKeyDown = (e) => {
|
||
// 检测回车
|
||
if (e.key === 'Enter') {
|
||
e.preventDefault()
|
||
if (props.isAgentMode) {
|
||
handleSendAgent()
|
||
} else {
|
||
handleCreateProject()
|
||
}
|
||
return
|
||
}
|
||
if (e.key === 'Backspace') {
|
||
const selection = window.getSelection()
|
||
if (selection.rangeCount > 0) {
|
||
const range = selection.getRangeAt(0)
|
||
if (range.collapsed) {
|
||
let nodeToDelete: Node | null = null
|
||
const startContainer = range.startContainer
|
||
const startOffset = range.startOffset
|
||
|
||
if (startContainer.nodeType === Node.TEXT_NODE) {
|
||
// Cursor at the end of a text node, check next sibling
|
||
if (startOffset === startContainer.length) {
|
||
nodeToDelete = startContainer.nextSibling
|
||
}
|
||
} else if (startContainer.nodeType === Node.ELEMENT_NODE) {
|
||
// Cursor positioned between child nodes
|
||
nodeToDelete = startContainer.childNodes[startOffset]
|
||
}
|
||
|
||
// 检查 nodeToDelete 是否存在以及是否为元素节点
|
||
if (
|
||
nodeToDelete &&
|
||
nodeToDelete.nodeType === Node.ELEMENT_NODE &&
|
||
(nodeToDelete as Element).classList
|
||
) {
|
||
const element = nodeToDelete as Element
|
||
const isEditorTag = element.classList.contains('editor-tag')
|
||
const isReportTag = element.classList.contains('report-tag')
|
||
const isCustomPlaceholder = element.classList.contains('custom-placeholder')
|
||
|
||
if (isEditorTag || isReportTag || isCustomPlaceholder) {
|
||
e.preventDefault()
|
||
element.remove()
|
||
|
||
// 如果删除的是 customPlaceholder,停止打字机效果
|
||
if (isCustomPlaceholder) {
|
||
stopTypewriter()
|
||
customPlaceholder.value = null
|
||
}
|
||
|
||
// 从 reportTags 中移除
|
||
const index = reportTags.value.indexOf(nodeToDelete as Element)
|
||
if (index > -1) {
|
||
reportTags.value.splice(index, 1)
|
||
}
|
||
|
||
// 删除标签后清理零宽字符并检查是否为空
|
||
nextTick(() => {
|
||
cleanupEditor()
|
||
})
|
||
return
|
||
}
|
||
}
|
||
nextTick(() => {
|
||
if (editorRef.value) {
|
||
const text = editorRef.value.textContent || ''
|
||
const cleanedText = text.replace(/[\u200B-\u200D\uFEFF]/g, '')
|
||
|
||
if (editorRef.value.innerHTML === '<br>' || !cleanedText.trim()) {
|
||
editorRef.value.innerHTML = ''
|
||
// 检查是否完全为空,需要显示占位符
|
||
if (editorRef.value.children.length === 0) {
|
||
reportTags.value = []
|
||
customPlaceholder.value = null
|
||
stopTypewriter()
|
||
}
|
||
}
|
||
}
|
||
})
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
const handleSendAgent = async () => {
|
||
if (props.generating) {
|
||
emits('pause')
|
||
return
|
||
}
|
||
if (!inputValue.value.trim()) return
|
||
const imageUrlList = uploadedImages.value.map((item) => item.path)
|
||
|
||
const payload = {
|
||
text: inputValue.value.trim(),
|
||
images: imageUrlList,
|
||
tempImages: uploadedImages.value
|
||
}
|
||
if (reportTags.value.length > 0) {
|
||
payload.useReport = true
|
||
}
|
||
emits('send', payload)
|
||
// 发送后清空图片列表
|
||
uploadedImages.value = []
|
||
// 发送后清空输入框
|
||
if (editorRef.value) {
|
||
editorRef.value.innerHTML = ''
|
||
}
|
||
inputValue.value = ''
|
||
}
|
||
// 监听 inputValue 外部变化
|
||
watch(inputValue, () => {
|
||
nextTick(() => {
|
||
autoResizeEditor()
|
||
})
|
||
})
|
||
|
||
// 初始化编辑器高度
|
||
onMounted(() => {
|
||
nextTick(() => {
|
||
autoResizeEditor()
|
||
})
|
||
})
|
||
|
||
const typeValue = ref<string>('')
|
||
const areaValue = ref<string>('')
|
||
const styleValue = ref<string>('')
|
||
const tempSelectedValue = ref<string>('')
|
||
const stylePopupVisible = ref(false)
|
||
|
||
const settingPopupVisible = ref(false)
|
||
const settingOptions = ref([
|
||
{ label: 'Input.settingOptions.first', value: 50 },
|
||
{ label: 'Input.settingOptions.second', value: 50 },
|
||
{ label: 'Input.settingOptions.third', value: 50 }
|
||
])
|
||
|
||
const openStylePopup = () => {
|
||
// 打开弹窗时初始化临时选中值为当前选中值
|
||
tempSelectedValue.value = styleValue.value
|
||
stylePopupVisible.value = true
|
||
}
|
||
|
||
const selectStyle = (value: string) => {
|
||
tempSelectedValue.value = value
|
||
}
|
||
|
||
const confirmStyle = () => {
|
||
// 点击确认后才真正赋值
|
||
styleValue.value = tempSelectedValue.value
|
||
stylePopupVisible.value = false
|
||
}
|
||
|
||
const confirmSetting = () => {
|
||
settingPopupVisible.value = false
|
||
}
|
||
const typeOptions = ref<any[]>([
|
||
{
|
||
label: 'Input.types.sofa',
|
||
value: 'Sofa'
|
||
},
|
||
{
|
||
label: 'Input.types.desk',
|
||
value: 'Desk'
|
||
},
|
||
{
|
||
label: 'Input.types.chair',
|
||
value: 'Chair'
|
||
}
|
||
])
|
||
const areaOptions = ref<any[]>(areaList)
|
||
const styleOptions = ref<any[]>(
|
||
styleKeys.map((key) => ({
|
||
label: key,
|
||
// label: `Input.styles.${key}`,
|
||
value: key
|
||
}))
|
||
)
|
||
|
||
const handleCreateProject = async () => {
|
||
if (!inputValue.value.trim()) {
|
||
return
|
||
}
|
||
|
||
const params = {
|
||
type: typeValue.value,
|
||
area: areaValue.value,
|
||
style: styleValue.value,
|
||
useReport: reportTags.value.length > 0,
|
||
temperature: 0.7
|
||
}
|
||
const projectres = await createProject(params)
|
||
// console.log('projectres', projectres)
|
||
projectStore.setId(projectres)
|
||
MyEvent.emit('updateProjectList')
|
||
// 保存初始数据到 store
|
||
agentStore.setInitialProjectData({
|
||
text: inputValue.value.trim(),
|
||
images: uploadedImages.value.map((item) => item.path),
|
||
tempImages: uploadedImages.value,
|
||
...params
|
||
})
|
||
|
||
// console.log('Create project with:', params)
|
||
router.push(`/home/agent/${projectres}`, { query: params })
|
||
uploadedImages.value = []
|
||
}
|
||
|
||
const showPreview = ref(false)
|
||
const previewUrl = ref('')
|
||
const previewImage = (url: string) => {
|
||
showPreview.value = true
|
||
previewUrl.value = url
|
||
}
|
||
|
||
// 暴露方法给父组件
|
||
defineExpose({
|
||
addReportTag
|
||
})
|
||
</script>
|
||
|
||
<style lang="less" scoped>
|
||
.report-btn {
|
||
position: absolute;
|
||
bottom: -7.4rem;
|
||
height: 4.4rem;
|
||
border-radius: 2.2rem;
|
||
width: 19.7rem;
|
||
padding: 0 2rem;
|
||
font-size: 1.8rem;
|
||
background-color: #fff;
|
||
border: 1.1px solid #f6f4ef1a;
|
||
cursor: pointer;
|
||
|
||
.c-svg {
|
||
width: 1.5rem;
|
||
height: 2rem;
|
||
}
|
||
}
|
||
.assist-input-wrapper {
|
||
min-height: 23.5rem;
|
||
max-height: 43.5rem;
|
||
width: 106.3rem;
|
||
|
||
margin: 0 auto;
|
||
padding: 0;
|
||
position: relative;
|
||
.animate-container {
|
||
overflow-y: hidden;
|
||
}
|
||
|
||
&:not(.agent) .animate-container {
|
||
box-shadow: 0px 0.5rem 1.4rem 0px #0000001a;
|
||
transition: all 0.3s ease;
|
||
border-radius: 2.8rem;
|
||
background-color: #fff;
|
||
border: 0.1rem solid #00000005;
|
||
&:not(.agent):hover {
|
||
box-shadow: 0px 0.5rem 3.36rem 2.2rem #f1ede999;
|
||
transform: translateY(-1rem);
|
||
}
|
||
}
|
||
.scroll-content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding: 3.4rem 1.7rem 1.7rem;
|
||
}
|
||
|
||
.editor-wrapper {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
position: relative;
|
||
min-height: 0;
|
||
}
|
||
|
||
.editor-placeholder {
|
||
position: absolute;
|
||
z-index: 0;
|
||
pointer-events: none;
|
||
top: 0;
|
||
left: 0;
|
||
padding: 0 1.4rem 1.4rem;
|
||
font-size: 2rem;
|
||
font-family: 'InterRegular';
|
||
font-weight: 400;
|
||
color: #999;
|
||
pointer-events: none;
|
||
white-space: pre-wrap;
|
||
word-wrap: break-word;
|
||
width: calc(100% - 2.8rem);
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.editor {
|
||
width: 100%;
|
||
flex: 1;
|
||
border: none;
|
||
outline: none;
|
||
padding: 0 1.4rem 1.4rem;
|
||
font-size: 1.8rem;
|
||
font-family: 'InterRegular';
|
||
font-weight: 400;
|
||
color: #000000;
|
||
overflow-y: auto;
|
||
overflow-x: hidden;
|
||
line-height: 2.8rem;
|
||
white-space: pre-wrap;
|
||
word-wrap: break-word;
|
||
&::-webkit-scrollbar {
|
||
width: 0;
|
||
display: none;
|
||
}
|
||
&::-webkit-scrollbar-thumb {
|
||
display: none;
|
||
}
|
||
&::-webkit-scrollbar-track {
|
||
display: none;
|
||
}
|
||
|
||
// 占位符
|
||
&:empty::before {
|
||
content: attr(placeholder);
|
||
color: #999;
|
||
pointer-events: none;
|
||
}
|
||
}
|
||
|
||
// 图片预览区域样式
|
||
.image-preview-list {
|
||
padding: 0 1.4rem 1rem;
|
||
column-gap: 1rem;
|
||
max-height: 15rem;
|
||
overflow-y: auto;
|
||
flex-shrink: 0;
|
||
|
||
.image-preview-item {
|
||
position: relative;
|
||
width: 8.6rem;
|
||
height: 8.6rem;
|
||
border-radius: 1.5rem;
|
||
overflow: hidden;
|
||
flex-shrink: 0;
|
||
border: 0.1rem solid #cdcdcd;
|
||
|
||
.preview-image {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
border-radius: 0.8rem;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.image-remove-btn {
|
||
position: absolute;
|
||
top: 0.6rem;
|
||
right: 0.6rem;
|
||
width: 1.6rem;
|
||
height: 1.6rem;
|
||
color: #fff;
|
||
border-radius: 50%;
|
||
// display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 1.2rem;
|
||
cursor: pointer;
|
||
display: none;
|
||
}
|
||
|
||
&:hover .image-remove-btn {
|
||
display: flex;
|
||
}
|
||
}
|
||
}
|
||
.operate {
|
||
flex-shrink: 0;
|
||
margin-top: auto;
|
||
padding: 0 1.7rem 1.7rem;
|
||
|
||
.left {
|
||
column-gap: 2rem;
|
||
}
|
||
.agent-operate {
|
||
width: 3.2rem;
|
||
height: 3.2rem;
|
||
cursor: pointer;
|
||
border-radius: 50%;
|
||
&:hover {
|
||
background-color: #f0f0f0;
|
||
}
|
||
}
|
||
.attach {
|
||
width: 4rem;
|
||
height: 4rem;
|
||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||
border-radius: 50%;
|
||
cursor: pointer;
|
||
img {
|
||
width: 1.65rem;
|
||
height: 1.86rem;
|
||
}
|
||
}
|
||
.el-select {
|
||
width: 13.9rem;
|
||
height: 4rem;
|
||
:deep(.el-select__wrapper) {
|
||
border-radius: 0.8rem;
|
||
height: 100%;
|
||
box-shadow: none;
|
||
border: 0.1rem solid rgba(0, 0, 0, 0.1);
|
||
font-weight: 500;
|
||
font-size: 1.4rem;
|
||
min-height: initial;
|
||
.el-select__placeholder {
|
||
color: #000;
|
||
}
|
||
.el-select__icon {
|
||
color: #000;
|
||
}
|
||
}
|
||
}
|
||
.fida-style-select-wrapper {
|
||
position: relative;
|
||
width: 13.9rem;
|
||
height: 4rem;
|
||
}
|
||
.fida-style-select-trigger {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
z-index: 1;
|
||
cursor: pointer;
|
||
}
|
||
.setting-icon {
|
||
width: 2.4rem;
|
||
height: 2.4rem;
|
||
cursor: pointer;
|
||
}
|
||
.create-btn {
|
||
background-color: #ff7a51;
|
||
height: 4rem;
|
||
width: 13rem;
|
||
color: #fff;
|
||
border-radius: 4.2rem;
|
||
font-family: 'MSemiBold';
|
||
font-weight: 600;
|
||
font-size: 1.28rem;
|
||
cursor: pointer;
|
||
.shining-icon {
|
||
width: 1.4rem;
|
||
height: 1.4rem;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
.input-option {
|
||
// padding: 0 1rem;
|
||
margin: 0 0.6rem;
|
||
padding: 0 0.8rem 0 1rem;
|
||
color: #0d0d0d;
|
||
font-weight: 510;
|
||
font-size: 1.3rem;
|
||
height: 3rem;
|
||
line-height: 3rem;
|
||
&.el-select-dropdown__item.is-hovering {
|
||
background-color: rgba(13, 13, 13, 0.02);
|
||
// border-radius: 0.6rem;
|
||
}
|
||
}
|
||
.agent {
|
||
padding: 1.2rem;
|
||
box-shadow: none;
|
||
border-radius: 1.5rem;
|
||
border: 0.1rem solid #0000001a;
|
||
.scroll-content {
|
||
padding: 0;
|
||
flex: 1;
|
||
overflow: auto;
|
||
.editor {
|
||
font-family: 'Regular';
|
||
font-weight: 400;
|
||
font-size: 1.4rem;
|
||
min-height: initial;
|
||
max-height: initial;
|
||
padding: 0;
|
||
height: 100%;
|
||
min-height: 5rem;
|
||
line-height: 1.4rem;
|
||
}
|
||
.editor-placeholder {
|
||
font-family: 'Regular';
|
||
font-size: 1.4rem;
|
||
padding: 0;
|
||
line-height: 1.4rem;
|
||
}
|
||
}
|
||
.operate {
|
||
padding: 1.2rem 0 0;
|
||
margin: 0;
|
||
.right {
|
||
display: flex;
|
||
align-items: center;
|
||
.sender-btn {
|
||
width: 3.2rem;
|
||
height: 3.2rem;
|
||
cursor: pointer;
|
||
background-color: #ff7a51;
|
||
border-radius: 50%;
|
||
&:hover {
|
||
background-color: #f8693d;
|
||
}
|
||
.sender-icon {
|
||
width: 1.3rem;
|
||
height: 1.3rem;
|
||
}
|
||
.sender-pause {
|
||
width: 1rem;
|
||
height: 1rem;
|
||
background-color: #fff;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</style>
|
||
|
||
<style lang="less">
|
||
.custom-placeholder {
|
||
color: #999;
|
||
margin-left: 0.5rem;
|
||
pointer-events: none;
|
||
}
|
||
.fida-style-select-popover {
|
||
width: 34.2rem !important;
|
||
padding: 0 !important;
|
||
border-radius: 0.6rem !important;
|
||
box-shadow: 0px 5px 20px 0px rgba(0, 0, 0, 0.15) !important;
|
||
background-color: #fff !important;
|
||
border: none !important;
|
||
}
|
||
|
||
.fida-style-popover-content {
|
||
padding: 2rem 2.4rem 2.4rem;
|
||
}
|
||
|
||
.fida-style-popover-header {
|
||
font-weight: 500;
|
||
font-size: 1.6rem;
|
||
color: #000;
|
||
margin-bottom: 2rem;
|
||
padding: 2rem 2.4rem !important;
|
||
// padding: 1.8rem 2rem 1.5rem;
|
||
// border-bottom: 0.1rem solid #f0f0f0;
|
||
}
|
||
|
||
.fida-style-popover-grid {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 1rem;
|
||
justify-content: center;
|
||
height: 28.5rem;
|
||
overflow-y: auto;
|
||
// display: grid;
|
||
// grid-template-columns: repeat(3, 1fr);
|
||
// gap: 1rem;
|
||
}
|
||
|
||
.fida-style-popover-item {
|
||
height: 9.1rem;
|
||
width: 9.1rem;
|
||
border-radius: 1.4rem;
|
||
cursor: pointer;
|
||
position: relative;
|
||
border: none;
|
||
|
||
.checked-item-icon {
|
||
position: absolute;
|
||
bottom: 0;
|
||
right: 0;
|
||
transform: translate(50%, 50%);
|
||
width: 2.4rem;
|
||
height: 2.4rem;
|
||
}
|
||
.style-bg {
|
||
width: 100%;
|
||
height: 100%;
|
||
border-radius: 1.4rem;
|
||
}
|
||
.fida-option-label {
|
||
font-weight: 500;
|
||
font-size: 1.2rem;
|
||
color: #fff;
|
||
text-align: center;
|
||
padding: 0.5rem;
|
||
position: absolute;
|
||
height: 100%;
|
||
width: 100%;
|
||
background-color: rgba(0, 0, 0, 0.3);
|
||
border-radius: 1.4rem;
|
||
}
|
||
&.is-selected {
|
||
border: 0.3rem solid #000;
|
||
.fida-option-label {
|
||
display: none;
|
||
}
|
||
}
|
||
}
|
||
|
||
.fida-style-popover-footer {
|
||
// border-top: 0.1rem solid #f0f0f0;
|
||
padding: 2.4rem 0 !important;
|
||
margin-top: 2.4rem;
|
||
.fida-confirm-btn {
|
||
margin: 0 auto;
|
||
width: 15.7rem;
|
||
height: 3.4rem;
|
||
line-height: 3.4rem;
|
||
background-color: #ff7a51;
|
||
color: #fff;
|
||
border: none;
|
||
border-radius: 3.8rem;
|
||
font-weight: 500;
|
||
font-size: 1.4rem;
|
||
cursor: pointer;
|
||
}
|
||
}
|
||
|
||
.fida-setting-popover {
|
||
padding: 0 !important;
|
||
border-radius: 0.6rem !important;
|
||
background-color: #fff !important;
|
||
border: none !important;
|
||
width: 25.6rem;
|
||
height: 23.9rem;
|
||
|
||
box-shadow: 0px 11px 20px 0px #0000001a;
|
||
border-radius: 0.6rem;
|
||
}
|
||
|
||
.fida-setting-popover-header {
|
||
font-weight: 400;
|
||
font-size: 1.4rem;
|
||
color: #000;
|
||
margin-bottom: 2rem !important;
|
||
}
|
||
.fida-setting-popover-content {
|
||
padding: 1.6rem 1.4rem 2.2rem !important;
|
||
}
|
||
|
||
.fida-setting-slider-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
row-gap: 1rem;
|
||
}
|
||
|
||
.fida-setting-slider-item {
|
||
.fida-slider-label {
|
||
font-weight: 400;
|
||
font-size: 1.2rem;
|
||
color: #000;
|
||
margin-bottom: 1rem;
|
||
}
|
||
.fida-slider-row {
|
||
column-gap: 2.6rem;
|
||
.el-slider {
|
||
flex: 1;
|
||
}
|
||
.fida-slider-value {
|
||
font-weight: 400;
|
||
font-size: 1.4rem;
|
||
color: #000;
|
||
}
|
||
}
|
||
}
|
||
.setting-popover-slider {
|
||
--el-slider-height: 0.4rem;
|
||
height: fit-content;
|
||
.el-slider__runway {
|
||
height: var(--el-slider-height);
|
||
background-color: #e8e8e8;
|
||
border-radius: 0.2rem;
|
||
}
|
||
.el-slider__bar {
|
||
height: var(--el-slider-height);
|
||
background-color: #000;
|
||
border-radius: 0.2rem;
|
||
}
|
||
.el-slider__button-wrapper {
|
||
width: fit-content;
|
||
height: fit-content;
|
||
top: 50%;
|
||
transform: translate(-50%, -50%);
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
.el-slider__button {
|
||
width: 1rem;
|
||
height: 1rem;
|
||
background-color: #000;
|
||
border-radius: 50%;
|
||
border: none;
|
||
}
|
||
.el-slider__stop {
|
||
display: none;
|
||
}
|
||
}
|
||
|
||
/* 动态添加的编辑器标签样式 */
|
||
.assist-input-wrapper .editor .editor-tag {
|
||
width: 15.6rem;
|
||
height: 3.1rem;
|
||
display: inline-flex;
|
||
border: 1px solid #0000001a;
|
||
font-weight: 500;
|
||
font-size: 1.3rem;
|
||
column-gap: 0;
|
||
margin: 0 0.5rem;
|
||
vertical-align: middle;
|
||
border-radius: 2.2rem;
|
||
&.restore {
|
||
width: auto;
|
||
max-width: 100%;
|
||
display: inline-flex;
|
||
border: none;
|
||
border-radius: 0.4rem;
|
||
background-color: rgba(0, 0, 0, 0.05);
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 0 0.9rem 0 0.7rem;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
span {
|
||
margin: 0 0.7rem 0 1.2rem;
|
||
&.restore-text {
|
||
flex: 1;
|
||
min-width: 0;
|
||
max-width: 52rem;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
}
|
||
|
||
.light-icon {
|
||
width: 0.9rem;
|
||
height: 1.2rem;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.close-icon {
|
||
width: 0.9rem;
|
||
height: 0.9rem;
|
||
cursor: pointer;
|
||
flex-shrink: 0;
|
||
&.restore {
|
||
width: 0.5rem;
|
||
height: 0.5rem;
|
||
}
|
||
}
|
||
.restore-icon {
|
||
width: 1.2rem;
|
||
height: 1.2rem;
|
||
}
|
||
}
|
||
|
||
.agent-plus-popover {
|
||
display: flex;
|
||
justify-content: flex-start;
|
||
background: transparent !important;
|
||
box-shadow: none !important;
|
||
border: none !important;
|
||
padding: 0 !important;
|
||
margin-bottom: -1rem;
|
||
width: fit-content !important;
|
||
min-width: initial !important;
|
||
//pointer-events: none;
|
||
.el-popper__arrow:before {
|
||
display: none;
|
||
}
|
||
.agent-modal {
|
||
// width: 14.6rem;
|
||
// height: 8.5rem;
|
||
|
||
row-gap: 1.2rem;
|
||
font-family: 'Medium';
|
||
font-weight: 500;
|
||
font-size: 1.3rem;
|
||
color: #222;
|
||
background-color: #fff;
|
||
border: 1px solid #d9d9d9;
|
||
box-shadow: 0px 6.53px 32.63px 0px #0000000d;
|
||
border-radius: 1rem;
|
||
padding: 1.2rem 1.4rem;
|
||
transform: translateX(calc(50% - 1.6rem));
|
||
.c-svg {
|
||
width: initial;
|
||
width: 1rem;
|
||
height: 1.3rem;
|
||
}
|
||
.file,
|
||
.report {
|
||
line-height: 1.8rem;
|
||
column-gap: 1rem;
|
||
cursor: pointer;
|
||
}
|
||
.file {
|
||
.file-icon {
|
||
width: 1.1rem;
|
||
height: 1.3rem;
|
||
}
|
||
}
|
||
.gap {
|
||
height: 0.05rem;
|
||
background-color: #d4d4d4;
|
||
}
|
||
}
|
||
}
|
||
</style>
|