chore: 代码格式化统一(空格、换行、属性排序、LF规范化)
This commit is contained in:
parent
b964c826ce
commit
e98ff3a2c4
@ -7,7 +7,8 @@
|
||||
"Bash(git -C \"D:/WebUI/Kexue/操作平台/AI_Painting_V2.0\" log --oneline --all -- src/stores/display.js)",
|
||||
"Bash(git -C \"D:/WebUI/Kexue/操作平台/AI_Painting_V2.0\" log --oneline --all -- src/components/dialogBox/index.vue)",
|
||||
"Bash(git -C \"D:/WebUI/Kexue/操作平台/AI_Painting_V2.0\" log --all --oneline --follow -p -- src/stores/display.js)",
|
||||
"Bash(git -C \"D:/WebUI/Kexue/操作平台/AI_Painting_V2.0\" log --all --oneline -p -- src/components/dialogBox/index.vue)"
|
||||
"Bash(git -C \"D:/WebUI/Kexue/操作平台/AI_Painting_V2.0\" log --all --oneline -p -- src/components/dialogBox/index.vue)",
|
||||
"Bash(npx eslint *)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -43,5 +43,5 @@ export const getUserInfo = () => {
|
||||
}
|
||||
|
||||
export const checkUsertoken = () => {
|
||||
return service.post(`/login/validateToken`)
|
||||
return service.post('/login/validateToken')
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="custom-popover" ref="popoverRef">
|
||||
<div class="popover-trigger" ref="triggerRef" @click.stop="togglePopover">
|
||||
<div ref="popoverRef" class="custom-popover">
|
||||
<div ref="triggerRef" class="popover-trigger" @click.stop="togglePopover">
|
||||
<slot name="reference" />
|
||||
</div>
|
||||
<Teleport to="body">
|
||||
@ -143,10 +143,10 @@ const handleClickOutside = (e) => {
|
||||
const contentEl = contentRef.value
|
||||
|
||||
if (
|
||||
triggerEl &&
|
||||
!triggerEl.contains(e.target) &&
|
||||
contentEl &&
|
||||
!contentEl.contains(e.target)
|
||||
triggerEl
|
||||
&& !triggerEl.contains(e.target)
|
||||
&& contentEl
|
||||
&& !contentEl.contains(e.target)
|
||||
) {
|
||||
visible.value = false
|
||||
window.__currentOpenPopoverId__ = null
|
||||
|
||||
@ -377,7 +377,7 @@ onBeforeUnmount(() => {
|
||||
|
||||
.option-group {
|
||||
/* margin-bottom: 10px; */
|
||||
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@ -9,8 +9,8 @@
|
||||
</div>
|
||||
<div class="close-btn" @click="handleClose">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="13" viewBox="0 0 12 13" fill="none">
|
||||
<path d="M1.28809 1.01469L10.9873 11.0052" stroke="#666666" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M10.6846 1.02413L0.985354 11.0147" stroke="#666666" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M1.28809 1.01469L10.9873 11.0052" stroke="#666666" stroke-width="2" stroke-linecap="round" />
|
||||
<path d="M10.6846 1.02413L0.985354 11.0147" stroke="#666666" stroke-width="2" stroke-linecap="round" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
@ -32,8 +32,8 @@
|
||||
ref="editableDivRef"
|
||||
contenteditable="true"
|
||||
class="custom-textarea"
|
||||
@input="handleInput"
|
||||
:data-placeholder="!inputText ? '请输入提示词或使用圆形/矩形工具' : ''"
|
||||
@input="handleInput"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
@ -48,7 +48,7 @@
|
||||
@click="currentShape = 'rectangle'"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none">
|
||||
<rect x="1.84961" y="1.84998" width="14.3" height="14.3" rx="1.5" :stroke="currentShape === 'rectangle' ? '#000F33' : '#888888'"/>
|
||||
<rect x="1.84961" y="1.84998" width="14.3" height="14.3" rx="1.5" :stroke="currentShape === 'rectangle' ? '#000F33' : '#888888'" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
@ -59,7 +59,7 @@
|
||||
@click="currentShape = 'circle'"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none">
|
||||
<circle cx="9.00039" cy="9.00002" r="7.6" :stroke="currentShape === 'circle' ? '#000F33' : '#888888'"/>
|
||||
<circle cx="9.00039" cy="9.00002" r="7.6" :stroke="currentShape === 'circle' ? '#000F33' : '#888888'" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
@ -68,22 +68,22 @@
|
||||
<!-- 上一步 -->
|
||||
<div class="shape-btn" @click="undo">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none">
|
||||
<path d="M1.64645 3.64645C1.45118 3.84171 1.45118 4.15829 1.64645 4.35355L4.82843 7.53553C5.02369 7.7308 5.34027 7.7308 5.53553 7.53553C5.7308 7.34027 5.7308 7.02369 5.53553 6.82843L2.70711 4L5.53553 1.17157C5.7308 0.976311 5.7308 0.659728 5.53553 0.464466C5.34027 0.269204 5.02369 0.269204 4.82843 0.464466L1.64645 3.64645ZM2 14.5C1.72386 14.5 1.5 14.7239 1.5 15C1.5 15.2761 1.72386 15.5 2 15.5V15V14.5ZM2 4V4.5H10.5V4V3.5H2V4ZM10.5 15V14.5H2V15V15.5H10.5V15ZM16 9.5H15.5C15.5 12.2614 13.2614 14.5 10.5 14.5V15V15.5C13.8137 15.5 16.5 12.8137 16.5 9.5H16ZM16 9.5H16.5C16.5 6.18629 13.8137 3.5 10.5 3.5V4V4.5C13.2614 4.5 15.5 6.73858 15.5 9.5H16Z" fill="#000F33"/>
|
||||
<path d="M1.64645 3.64645C1.45118 3.84171 1.45118 4.15829 1.64645 4.35355L4.82843 7.53553C5.02369 7.7308 5.34027 7.7308 5.53553 7.53553C5.7308 7.34027 5.7308 7.02369 5.53553 6.82843L2.70711 4L5.53553 1.17157C5.7308 0.976311 5.7308 0.659728 5.53553 0.464466C5.34027 0.269204 5.02369 0.269204 4.82843 0.464466L1.64645 3.64645ZM2 14.5C1.72386 14.5 1.5 14.7239 1.5 15C1.5 15.2761 1.72386 15.5 2 15.5V15V14.5ZM2 4V4.5H10.5V4V3.5H2V4ZM10.5 15V14.5H2V15V15.5H10.5V15ZM16 9.5H15.5C15.5 12.2614 13.2614 14.5 10.5 14.5V15V15.5C13.8137 15.5 16.5 12.8137 16.5 9.5H16ZM16 9.5H16.5C16.5 6.18629 13.8137 3.5 10.5 3.5V4V4.5C13.2614 4.5 15.5 6.73858 15.5 9.5H16Z" fill="#000F33" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- 下一步 -->
|
||||
<div class="shape-btn" @click="redo">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none">
|
||||
<path d="M16.3536 3.64645C16.5488 3.84171 16.5488 4.15829 16.3536 4.35355L13.1716 7.53553C12.9763 7.7308 12.6597 7.7308 12.4645 7.53553C12.2692 7.34027 12.2692 7.02369 12.4645 6.82843L15.2929 4L12.4645 1.17157C12.2692 0.976311 12.2692 0.659728 12.4645 0.464466C12.6597 0.269204 12.9763 0.269204 13.1716 0.464466L16.3536 3.64645ZM16 13.5C16.2761 13.5 16.5 13.7239 16.5 14C16.5 14.2761 16.2761 14.5 16 14.5V14V13.5ZM16 4V4.5H8V4V3.5H16V4ZM8 14V13.5H16V14V14.5H8V14ZM3 9H3.5C3.5 11.4853 5.51472 13.5 8 13.5V14V14.5C4.96243 14.5 2.5 12.0376 2.5 9H3ZM3 9H2.5C2.5 5.96243 4.96243 3.5 8 3.5V4V4.5C5.51472 4.5 3.5 6.51472 3.5 9H3Z" fill="#000F33"/>
|
||||
<path d="M16.3536 3.64645C16.5488 3.84171 16.5488 4.15829 16.3536 4.35355L13.1716 7.53553C12.9763 7.7308 12.6597 7.7308 12.4645 7.53553C12.2692 7.34027 12.2692 7.02369 12.4645 6.82843L15.2929 4L12.4645 1.17157C12.2692 0.976311 12.2692 0.659728 12.4645 0.464466C12.6597 0.269204 12.9763 0.269204 13.1716 0.464466L16.3536 3.64645ZM16 13.5C16.2761 13.5 16.5 13.7239 16.5 14C16.5 14.2761 16.2761 14.5 16 14.5V14V13.5ZM16 4V4.5H8V4V3.5H16V4ZM8 14V13.5H16V14V14.5H8V14ZM3 9H3.5C3.5 11.4853 5.51472 13.5 8 13.5V14V14.5C4.96243 14.5 2.5 12.0376 2.5 9H3ZM3 9H2.5C2.5 5.96243 4.96243 3.5 8 3.5V4V4.5C5.51472 4.5 3.5 6.51472 3.5 9H3Z" fill="#000F33" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- 删除 -->
|
||||
<div class="shape-btn" @click="deleteShape">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none">
|
||||
<path d="M1.7998 3.60002H4.4998M16.1998 3.60002H13.4998M13.4998 3.60002V2.90003C13.4998 1.79546 12.6044 0.900024 11.4998 0.900024H6.4998C5.39523 0.900024 4.4998 1.79545 4.4998 2.90002V3.60002M13.4998 3.60002H4.4998" stroke="#000F33" stroke-linecap="round"/>
|
||||
<path d="M3.59961 6.29999V14.2C3.59961 15.3046 4.49504 16.2 5.59961 16.2H12.3996C13.5042 16.2 14.3996 15.3046 14.3996 14.2V6.29999M7.19961 7.19999V14.4M10.7996 7.19999V14.4" stroke="#000F33" stroke-linecap="round"/>
|
||||
<path d="M1.7998 3.60002H4.4998M16.1998 3.60002H13.4998M13.4998 3.60002V2.90003C13.4998 1.79546 12.6044 0.900024 11.4998 0.900024H6.4998C5.39523 0.900024 4.4998 1.79545 4.4998 2.90002V3.60002M13.4998 3.60002H4.4998" stroke="#000F33" stroke-linecap="round" />
|
||||
<path d="M3.59961 6.29999V14.2C3.59961 15.3046 4.49504 16.2 5.59961 16.2H12.3996C13.5042 16.2 14.3996 15.3046 14.3996 14.2V6.29999M7.19961 7.19999V14.4M10.7996 7.19999V14.4" stroke="#000F33" stroke-linecap="round" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
@ -103,8 +103,8 @@
|
||||
<span class="brush-panel-title">请输入替换内容描述</span>
|
||||
<div class="brush-panel-close" @click="closeBrushPanel">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="13" viewBox="0 0 12 13" fill="none">
|
||||
<path d="M1.28809 1.01468L10.9873 11.0052" stroke="#666666" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M10.6846 1.02411L0.985354 11.0147" stroke="#666666" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M1.28809 1.01468L10.9873 11.0052" stroke="#666666" stroke-width="2" stroke-linecap="round" />
|
||||
<path d="M10.6846 1.02411L0.985354 11.0147" stroke="#666666" stroke-width="2" stroke-linecap="round" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
@ -114,8 +114,8 @@
|
||||
ref="brushTextareaRef"
|
||||
contenteditable="true"
|
||||
class="brush-textarea"
|
||||
@input="handleBrushInput"
|
||||
:data-placeholder="!currentShapeDescription ? '请输入描述...' : ''"
|
||||
@input="handleBrushInput"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
@ -135,7 +135,7 @@
|
||||
<img :src="img" alt="参考图" />
|
||||
<div class="reference-image-delete" @click.stop="removeReferenceImage(index)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="7" height="7" viewBox="0 0 7 7" fill="none">
|
||||
<path d="M0.5 0.5L6.5 6.5M6.5 0.5L0.5 6.5" stroke="white" stroke-linecap="round"/>
|
||||
<path d="M0.5 0.5L6.5 6.5M6.5 0.5L0.5 6.5" stroke="white" stroke-linecap="round" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
@ -145,7 +145,7 @@
|
||||
@click="handleUploadReference"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 15 15" fill="none">
|
||||
<path d="M7.49316 0C7.76931 3.42279e-07 7.99316 0.223857 7.99316 0.5V6.99316H14.5C14.7761 6.99316 15 7.21702 15 7.49316C14.9999 7.76925 14.7761 7.99316 14.5 7.99316H7.99316V14.5C7.99316 14.7761 7.76931 15 7.49316 15C7.21702 15 6.99316 14.7761 6.99316 14.5V7.99316H0.5C0.223898 7.99316 6.59601e-05 7.76925 0 7.49316C0 7.21702 0.223858 6.99316 0.5 6.99316H6.99316V0.5C6.99316 0.223857 7.21702 1.20706e-08 7.49316 0Z" fill="#000F33"/>
|
||||
<path d="M7.49316 0C7.76931 3.42279e-07 7.99316 0.223857 7.99316 0.5V6.99316H14.5C14.7761 6.99316 15 7.21702 15 7.49316C14.9999 7.76925 14.7761 7.99316 14.5 7.99316H7.99316V14.5C7.99316 14.7761 7.76931 15 7.49316 15C7.21702 15 6.99316 14.7761 6.99316 14.5V7.99316H0.5C0.223898 7.99316 6.59601e-05 7.76925 0 7.49316C0 7.21702 0.223858 6.99316 0.5 6.99316H6.99316V0.5C6.99316 0.223857 7.21702 1.20706e-08 7.49316 0Z" fill="#000F33" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
@ -160,10 +160,10 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { generate } from '@/utils/taskPolling'
|
||||
import { useDisplayStore } from '@/stores'
|
||||
import request from '@/utils/request'
|
||||
import { getModelId } from '@/utils/modelApi'
|
||||
import request from '@/utils/request'
|
||||
import { generate } from '@/utils/taskPolling'
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
@ -240,11 +240,11 @@ const updateCurrentEditingContent = (description) => {
|
||||
const colorIndex = currentEditingShapeIndex.value >= 0 ? currentEditingShapeIndex.value : 0
|
||||
const colorName = colorNames[colorIndex % colorNames.length]
|
||||
const isFirstShape = shapes.value.length === 0 || (currentEditingShapeIndex.value === 0 && shapes.value.length === 1)
|
||||
|
||||
|
||||
const currentShape = currentEditingShapeIndex.value >= 0 ? shapes.value[currentEditingShapeIndex.value] : null
|
||||
const shapeType = currentShape ? currentShape.type : 'rectangle'
|
||||
const shapeWord = shapeType === 'circle' ? '圈' : '框'
|
||||
|
||||
|
||||
if (selectedReferenceImages.value.length > 0) {
|
||||
const imageIndex = allReferenceImages.value.indexOf(selectedReferenceImages.value[0]) + 2
|
||||
const prefix = isFirstShape ? '' : ','
|
||||
@ -268,7 +268,7 @@ watch(() => props.visible, (newVal) => {
|
||||
historyIndex.value = -1
|
||||
promptHistory.value = []
|
||||
promptHistoryIndex.value = -1
|
||||
allReferenceImages.value = props.referenceImages.map(img => img.url || img)
|
||||
allReferenceImages.value = props.referenceImages.map((img) => img.url || img)
|
||||
brushPanelVisible.value = false
|
||||
isPanelOpen.value = false
|
||||
currentShapeDescription.value = ''
|
||||
@ -284,19 +284,19 @@ watch(() => props.visible, (newVal) => {
|
||||
const initCanvas = () => {
|
||||
const canvas = canvasRef.value
|
||||
if (!canvas) return
|
||||
|
||||
|
||||
const ctx = canvas.getContext('2d')
|
||||
const container = canvas.parentElement
|
||||
const containerWidth = container.clientWidth
|
||||
const containerHeight = container.clientHeight
|
||||
|
||||
|
||||
if (currentImage.value) {
|
||||
let imageUrl = currentImage.value
|
||||
|
||||
|
||||
if (!imageUrl.startsWith('data:')) {
|
||||
imageUrl = imageUrl.replace('https://sxwz.xueai.art', 'https://talkingdraw.xueai.art')
|
||||
}
|
||||
|
||||
|
||||
if (imageUrl.startsWith('data:')) {
|
||||
const img = new Image()
|
||||
img.onload = () => {
|
||||
@ -310,8 +310,8 @@ const initCanvas = () => {
|
||||
img.src = imageUrl
|
||||
} else {
|
||||
fetch(imageUrl)
|
||||
.then(res => res.blob())
|
||||
.then(blob => {
|
||||
.then((res) => res.blob())
|
||||
.then((blob) => {
|
||||
const img = new Image()
|
||||
img.onload = () => {
|
||||
const imgScale = Math.min(containerWidth / img.width, containerHeight / img.height)
|
||||
@ -363,7 +363,7 @@ const handleMouseDown = (e) => {
|
||||
|
||||
const handleMouseMove = (e) => {
|
||||
if (!isDrawing.value) return
|
||||
|
||||
|
||||
const canvas = canvasRef.value
|
||||
const ctx = canvas.getContext('2d')
|
||||
const ratio = canvas.width / canvas.offsetWidth
|
||||
@ -373,15 +373,15 @@ const handleMouseMove = (e) => {
|
||||
const savedStartY = startY.value
|
||||
const savedCurrentX = currentX
|
||||
const savedCurrentY = currentY
|
||||
|
||||
|
||||
if (bgImage.value) {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||
ctx.drawImage(bgImage.value, 0, 0)
|
||||
|
||||
shapes.value.forEach(shape => {
|
||||
|
||||
shapes.value.forEach((shape) => {
|
||||
drawShape(ctx, shape)
|
||||
})
|
||||
|
||||
|
||||
const currentShapeData = {
|
||||
type: currentShape.value,
|
||||
startX: savedStartX,
|
||||
@ -396,10 +396,10 @@ const handleMouseMove = (e) => {
|
||||
ctx.fillStyle = '#fff'
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||
|
||||
shapes.value.forEach(shape => {
|
||||
shapes.value.forEach((shape) => {
|
||||
drawShape(ctx, shape)
|
||||
})
|
||||
|
||||
|
||||
const currentShapeData = {
|
||||
type: currentShape.value,
|
||||
startX: startX.value,
|
||||
@ -414,34 +414,34 @@ const handleMouseMove = (e) => {
|
||||
|
||||
const handleMouseUp = (e) => {
|
||||
if (!isDrawing.value) return
|
||||
|
||||
|
||||
const canvas = canvasRef.value
|
||||
const ratio = canvas.width / canvas.offsetWidth
|
||||
const endX = e.offsetX * ratio
|
||||
const endY = e.offsetY * ratio
|
||||
|
||||
|
||||
if (shapes.value.length >= maxShapes) {
|
||||
ElMessage.warning('最多只能画5笔')
|
||||
isDrawing.value = false
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
const colorIndex = shapes.value.length
|
||||
const newShape = {
|
||||
type: currentShape.value,
|
||||
startX: startX.value,
|
||||
startY: startY.value,
|
||||
endX: endX,
|
||||
endY: endY,
|
||||
endX,
|
||||
endY,
|
||||
color: shapeColors[colorIndex],
|
||||
description: '',
|
||||
referenceImages: []
|
||||
}
|
||||
shapes.value.push(newShape)
|
||||
currentEditingShapeIndex.value = shapes.value.length - 1
|
||||
|
||||
|
||||
isDrawing.value = false
|
||||
|
||||
|
||||
brushPanelVisible.value = true
|
||||
isPanelOpen.value = true
|
||||
currentShapeDescription.value = ''
|
||||
@ -452,7 +452,7 @@ const saveHistory = () => {
|
||||
history.value = history.value.slice(0, historyIndex.value + 1)
|
||||
history.value.push([...shapes.value])
|
||||
historyIndex.value = history.value.length - 1
|
||||
|
||||
|
||||
promptHistory.value = promptHistory.value.slice(0, promptHistoryIndex.value + 1)
|
||||
promptHistory.value.push(inputText.value)
|
||||
promptHistoryIndex.value = promptHistory.value.length - 1
|
||||
@ -463,7 +463,7 @@ const undo = () => {
|
||||
historyIndex.value--
|
||||
shapes.value = historyIndex.value >= 0 ? [...history.value[historyIndex.value]] : []
|
||||
redrawCanvas()
|
||||
|
||||
|
||||
promptHistoryIndex.value--
|
||||
inputText.value = promptHistoryIndex.value >= 0 ? promptHistory.value[promptHistoryIndex.value] : ''
|
||||
if (editableDivRef.value) {
|
||||
@ -477,7 +477,7 @@ const redo = () => {
|
||||
historyIndex.value++
|
||||
shapes.value = [...history.value[historyIndex.value]]
|
||||
redrawCanvas()
|
||||
|
||||
|
||||
promptHistoryIndex.value++
|
||||
inputText.value = promptHistory.value[promptHistoryIndex.value]
|
||||
if (editableDivRef.value) {
|
||||
@ -493,11 +493,11 @@ const deleteShape = () => {
|
||||
promptHistoryIndex.value = -1
|
||||
history.value = []
|
||||
historyIndex.value = -1
|
||||
|
||||
|
||||
if (editableDivRef.value) {
|
||||
editableDivRef.value.innerHTML = ''
|
||||
}
|
||||
|
||||
|
||||
redrawCanvas()
|
||||
}
|
||||
|
||||
@ -505,17 +505,17 @@ const redrawCanvas = () => {
|
||||
const canvas = canvasRef.value
|
||||
if (!canvas) return
|
||||
const ctx = canvas.getContext('2d')
|
||||
|
||||
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||
|
||||
|
||||
if (bgImage.value) {
|
||||
ctx.drawImage(bgImage.value, 0, 0)
|
||||
} else {
|
||||
ctx.fillStyle = '#fff'
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||
}
|
||||
|
||||
shapes.value.forEach(shape => {
|
||||
|
||||
shapes.value.forEach((shape) => {
|
||||
drawShape(ctx, shape)
|
||||
})
|
||||
}
|
||||
@ -523,14 +523,14 @@ const redrawCanvas = () => {
|
||||
const drawShape = (ctx, shape) => {
|
||||
ctx.strokeStyle = shape.color || '#ff0000'
|
||||
ctx.lineWidth = 2
|
||||
|
||||
|
||||
if (shape.type === 'rectangle') {
|
||||
const width = shape.endX - shape.startX
|
||||
const height = shape.endY - shape.startY
|
||||
ctx.strokeRect(shape.startX, shape.startY, width, height)
|
||||
} else if (shape.type === 'circle') {
|
||||
const radius = Math.sqrt(
|
||||
Math.pow(shape.endX - shape.startX, 2) + Math.pow(shape.endY - shape.startY, 2)
|
||||
(shape.endX - shape.startX) ** 2 + (shape.endY - shape.startY) ** 2
|
||||
)
|
||||
ctx.beginPath()
|
||||
ctx.arc(shape.startX, shape.startY, radius, 0, Math.PI * 2)
|
||||
@ -553,7 +553,7 @@ const removeReferenceImage = (index) => {
|
||||
const selectReferenceImage = (index) => {
|
||||
const img = allReferenceImages.value[index]
|
||||
const existingIndex = selectedReferenceImages.value.indexOf(img)
|
||||
|
||||
|
||||
if (existingIndex !== -1) {
|
||||
selectedReferenceImages.value = []
|
||||
} else {
|
||||
@ -569,14 +569,14 @@ const handleUploadReference = () => {
|
||||
input.onchange = async (e) => {
|
||||
const files = Array.from(e.target.files)
|
||||
const remainingSlots = 5 - allReferenceImages.value.length
|
||||
|
||||
|
||||
if (remainingSlots <= 0) {
|
||||
ElMessage.warning('最多只能上传5张图片')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
const filesToUpload = files.slice(0, remainingSlots)
|
||||
|
||||
|
||||
const readFileAsDataURL = (file) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
@ -585,7 +585,7 @@ const handleUploadReference = () => {
|
||||
reader.readAsDataURL(file)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
const imageDataUrls = await Promise.all(filesToUpload.map(readFileAsDataURL))
|
||||
allReferenceImages.value.push(...imageDataUrls)
|
||||
}
|
||||
@ -597,7 +597,7 @@ const closeBrushPanel = () => {
|
||||
shapes.value.splice(currentEditingShapeIndex.value, 1)
|
||||
redrawCanvas()
|
||||
}
|
||||
|
||||
|
||||
brushPanelVisible.value = false
|
||||
isPanelOpen.value = false
|
||||
currentShapeDescription.value = ''
|
||||
@ -611,7 +611,7 @@ const handleBrushConfirm = () => {
|
||||
shapes.value[currentEditingShapeIndex.value].description = currentShapeDescription.value
|
||||
shapes.value[currentEditingShapeIndex.value].referenceImages = [...selectedReferenceImages.value]
|
||||
}
|
||||
|
||||
|
||||
if (currentEditingContent.value) {
|
||||
if (editableDivRef.value) {
|
||||
const currentContent = editableDivRef.value.innerHTML
|
||||
@ -621,9 +621,9 @@ const handleBrushConfirm = () => {
|
||||
inputText.value += currentEditingContent.value
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
saveHistory()
|
||||
|
||||
|
||||
brushPanelVisible.value = false
|
||||
isPanelOpen.value = false
|
||||
currentShapeDescription.value = ''
|
||||
@ -635,10 +635,10 @@ const getImageAspectRatio = () => {
|
||||
if (!bgImage.value) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
const width = bgImage.value.width
|
||||
const height = bgImage.value.height
|
||||
|
||||
|
||||
const aspectRatios = [
|
||||
{ ratio: '4:3', value: 4 / 3 },
|
||||
{ ratio: '16:9', value: 16 / 9 },
|
||||
@ -648,12 +648,12 @@ const getImageAspectRatio = () => {
|
||||
{ ratio: '2:3', value: 2 / 3 },
|
||||
{ ratio: '3:2', value: 3 / 2 }
|
||||
]
|
||||
|
||||
|
||||
const currentRatio = width / height
|
||||
|
||||
|
||||
let closest = aspectRatios[0]
|
||||
let minDiff = Math.abs(currentRatio - closest.value)
|
||||
|
||||
|
||||
for (const item of aspectRatios) {
|
||||
const diff = Math.abs(currentRatio - item.value)
|
||||
if (diff < minDiff) {
|
||||
@ -661,7 +661,7 @@ const getImageAspectRatio = () => {
|
||||
closest = item
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
@ -675,34 +675,34 @@ const handleSend = async () => {
|
||||
ElMessage.error('请输入提示词')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
isSending.value = true
|
||||
|
||||
|
||||
try {
|
||||
const canvas = canvasRef.value
|
||||
const imageData = canvas.toDataURL('image/png')
|
||||
|
||||
|
||||
const imgs = []
|
||||
if (imageData) {
|
||||
imgs.push({ name: 'image_1', url: imageData })
|
||||
}
|
||||
|
||||
|
||||
allReferenceImages.value.forEach((img, index) => {
|
||||
imgs.push({ name: `image_${index + 2}`, url: img })
|
||||
})
|
||||
|
||||
|
||||
const uploadImg = async (imgItem) => {
|
||||
if (!imgItem.url.startsWith('data:') && !imgItem.url.startsWith('blob:')) {
|
||||
return imgItem
|
||||
}
|
||||
|
||||
|
||||
const response = await fetch(imgItem.url)
|
||||
const blob = await response.blob()
|
||||
const file = new File([blob], `${imgItem.name}.png`, { type: 'image/png' })
|
||||
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
|
||||
try {
|
||||
const result = await request({
|
||||
url: import.meta.env.VITE_API_WORKFLOW_UPLOAD,
|
||||
@ -721,11 +721,11 @@ const handleSend = async () => {
|
||||
return imgItem
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const uploadedImgs = await Promise.all(imgs.map(uploadImg))
|
||||
|
||||
|
||||
const proportion = getImageAspectRatio()
|
||||
|
||||
|
||||
const modelId = await getModelId(props.type, 'GPT')
|
||||
|
||||
const generateData = {
|
||||
@ -743,20 +743,20 @@ const handleSend = async () => {
|
||||
modelId,
|
||||
quantity: 1,
|
||||
params: [
|
||||
{ name: 'prompt', data: inputText.value + '并且去除掉图1中的框' },
|
||||
{ name: 'prompt', data: `${inputText.value}并且去除掉图1中的框` },
|
||||
{ name: 'index', data: 1 },
|
||||
{ name: 'proportion', data: proportion?.aspectRatio || '4:3' },
|
||||
{ name: 'proportion', data: proportion?.aspectRatio || '4:3' }
|
||||
],
|
||||
imgs: uploadedImgs,
|
||||
request: JSON.stringify(generateData)
|
||||
}
|
||||
|
||||
|
||||
emit('send', {
|
||||
image: imageData,
|
||||
text: inputText.value,
|
||||
shapes: shapes.value
|
||||
})
|
||||
|
||||
|
||||
await generate(data, generateData)
|
||||
handleClose()
|
||||
} finally {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<Transition name="slide-up">
|
||||
<div class="input-container" :class="{ generate : !props.isGenerate }" @click="handleContainerClick">
|
||||
<div class="input-container" :class="{ generate: !props.isGenerate }" @click="handleContainerClick">
|
||||
<div v-if="!props.isGenerate" class="title">{{ platform.label }}</div>
|
||||
|
||||
<div class="sender-top">
|
||||
@ -11,8 +11,8 @@
|
||||
<div v-show="showUploader" class="upload-img-container">
|
||||
<div class="reference-diagram">
|
||||
<component
|
||||
v-if="platform.ImageUploader"
|
||||
:is="platform.ImageUploader"
|
||||
v-if="platform.ImageUploader"
|
||||
ref="referenceDiagramRef"
|
||||
v-model="referenceImages"
|
||||
v-bind="uploaderBindings"
|
||||
@ -34,11 +34,11 @@
|
||||
<div class="prefix-self-wrap">
|
||||
<component
|
||||
:is="platform.ModelSelector"
|
||||
:modelValue="platform.model.value"
|
||||
@update:modelValue="platform.model.value = $event"
|
||||
:typeValue="platform.modelType.value"
|
||||
@update:typeValue="platform.modelType.value = $event"
|
||||
:model-value="platform.model.value"
|
||||
:type-value="platform.modelType.value"
|
||||
v-bind="(platform.modelSelectorProps && platform.modelSelectorProps()) || {}"
|
||||
@update:model-value="platform.model.value = $event"
|
||||
@update:type-value="platform.modelType.value = $event"
|
||||
/>
|
||||
<template v-for="ctrl in visibleControls" :key="ctrl.name">
|
||||
<component
|
||||
@ -62,18 +62,17 @@
|
||||
</div>
|
||||
</template>
|
||||
</Sender>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Sender } from 'vue-element-plus-x'
|
||||
import { useDisplayStore } from '@/stores'
|
||||
import { generate } from '@/utils/taskPolling'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { createPlatform } from '@/platforms/registry.js'
|
||||
import { useDisplayStore } from '@/stores'
|
||||
import { getModelId } from '@/utils/modelApi'
|
||||
import { generate } from '@/utils/taskPolling'
|
||||
|
||||
// 确保平台包被加载(触发自注册)
|
||||
import '@/platforms/painting/index.js'
|
||||
@ -82,7 +81,7 @@ import '@/platforms/video/index.js'
|
||||
const props = defineProps({
|
||||
isGenerate: { type: Boolean, default: false },
|
||||
generate: { type: Boolean, default: false },
|
||||
type: { type: String, default: 'Painting' },
|
||||
type: { type: String, default: 'Painting' }
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
@ -99,7 +98,7 @@ const getCurrentConfig = () => {
|
||||
|
||||
const visibleControls = computed(() => {
|
||||
const config = getCurrentConfig()
|
||||
return platform.value.controls.filter(c => c.show(config))
|
||||
return platform.value.controls.filter((c) => c.show(config))
|
||||
})
|
||||
|
||||
const showUploader = computed(() => {
|
||||
@ -148,7 +147,7 @@ const handleStart = async () => {
|
||||
modelType: p.modelType.value,
|
||||
prompt: prompt.value,
|
||||
referenceImages: referenceImages.value,
|
||||
modelParams: body,
|
||||
modelParams: body
|
||||
}
|
||||
|
||||
const data = {
|
||||
@ -157,7 +156,7 @@ const handleStart = async () => {
|
||||
modelName: p.model.value,
|
||||
modelId: modelId || '',
|
||||
body,
|
||||
request: JSON.stringify(generateData),
|
||||
request: JSON.stringify(generateData)
|
||||
}
|
||||
|
||||
await generate(data, generateData)
|
||||
@ -190,7 +189,7 @@ watch(
|
||||
async ([newModel, newModelType]) => {
|
||||
if (!newModel) return
|
||||
await platform.value.loadConfig(newModel, newModelType)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// 平台切换 → 设置默认模型 + 预加载模型列表
|
||||
|
||||
@ -100,6 +100,8 @@ const props = defineProps({
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['scroll', 'scroll-start', 'scroll-end', 'visible-change'])
|
||||
|
||||
const computedData = computed(() => {
|
||||
return props.data.length > 0 ? props.data : props.items
|
||||
})
|
||||
@ -114,8 +116,6 @@ const computedBuffer = computed(() => {
|
||||
return props.buffer !== 3 ? props.buffer : props.bufferSize
|
||||
})
|
||||
|
||||
const emit = defineEmits(['scroll', 'scroll-start', 'scroll-end', 'visible-change'])
|
||||
|
||||
const containerRef = ref(null)
|
||||
const wrapperRef = ref(null)
|
||||
const renderContainerRef = ref(null)
|
||||
@ -140,27 +140,27 @@ const containerStyle = computed(() => {
|
||||
const totalHeight = computed(() => {
|
||||
let height = 0
|
||||
const len = computedData.value.length
|
||||
|
||||
|
||||
for (let i = 0; i < len; i++) {
|
||||
const cachedHeight = itemHeights.value.get(i)
|
||||
height += cachedHeight !== undefined ? cachedHeight : props.estimatedHeight
|
||||
}
|
||||
|
||||
|
||||
height += props.bottomPlaceholderHeight
|
||||
|
||||
|
||||
return height
|
||||
})
|
||||
|
||||
const getItemPosition = (index) => {
|
||||
let offset = 0
|
||||
|
||||
|
||||
for (let i = 0; i < index; i++) {
|
||||
const cachedHeight = itemHeights.value.get(i)
|
||||
offset += cachedHeight !== undefined ? cachedHeight : props.estimatedHeight
|
||||
}
|
||||
|
||||
|
||||
const height = itemHeights.value.get(index) ?? props.estimatedHeight
|
||||
|
||||
|
||||
return { offset, height }
|
||||
}
|
||||
|
||||
@ -173,73 +173,73 @@ const visibleRange = computed(() => {
|
||||
if (!renderContainerRef.value || computedData.value.length === 0) {
|
||||
return { start: 0, end: 0, offset: 0 }
|
||||
}
|
||||
|
||||
|
||||
const viewportHeight = containerHeight.value
|
||||
const currentScrollTop = scrollTop.value
|
||||
const bufferCount = computedBuffer.value
|
||||
|
||||
|
||||
let startIndex = 0
|
||||
let endIndex = computedData.value.length - 1
|
||||
let startOffset = 0
|
||||
|
||||
|
||||
let offset = 0
|
||||
for (let i = 0; i < computedData.value.length; i++) {
|
||||
const cachedHeight = itemHeights.value.get(i)
|
||||
const height = cachedHeight !== undefined ? cachedHeight : props.estimatedHeight
|
||||
|
||||
|
||||
if (offset + height > currentScrollTop) {
|
||||
startIndex = Math.max(0, i - bufferCount)
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
offset += height
|
||||
}
|
||||
|
||||
|
||||
startOffset = 0
|
||||
for (let i = 0; i < startIndex; i++) {
|
||||
const cachedHeight = itemHeights.value.get(i)
|
||||
startOffset += cachedHeight !== undefined ? cachedHeight : props.estimatedHeight
|
||||
}
|
||||
|
||||
|
||||
offset = startOffset
|
||||
endIndex = startIndex
|
||||
for (let i = startIndex; i < computedData.value.length; i++) {
|
||||
const cachedHeight = itemHeights.value.get(i)
|
||||
const height = cachedHeight !== undefined ? cachedHeight : props.estimatedHeight
|
||||
|
||||
|
||||
offset += height
|
||||
|
||||
|
||||
if (offset > currentScrollTop + viewportHeight) {
|
||||
endIndex = Math.min(computedData.value.length - 1, i + bufferCount)
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
endIndex = i
|
||||
}
|
||||
|
||||
|
||||
return { start: Math.min(startIndex, endIndex), end: Math.max(startIndex, endIndex), offset: startOffset }
|
||||
})
|
||||
|
||||
const visibleItems = computed(() => {
|
||||
const { start, end, offset } = visibleRange.value
|
||||
const items = []
|
||||
|
||||
|
||||
let currentOffset = offset
|
||||
|
||||
|
||||
for (let i = start; i <= end && i < computedData.value.length; i++) {
|
||||
const cachedHeight = itemHeights.value.get(i)
|
||||
const height = cachedHeight !== undefined ? cachedHeight : props.estimatedHeight
|
||||
|
||||
|
||||
items.push({
|
||||
item: computedData.value[i],
|
||||
index: i,
|
||||
offset: currentOffset + props.bottomPlaceholderHeight,
|
||||
height
|
||||
})
|
||||
|
||||
|
||||
currentOffset += height
|
||||
}
|
||||
|
||||
|
||||
return items
|
||||
})
|
||||
|
||||
@ -275,7 +275,7 @@ const bottomPlaceholderStyle = computed(() => ({
|
||||
top: 0,
|
||||
width: '100%',
|
||||
height: `${props.bottomPlaceholderHeight}px`,
|
||||
transform: `translateY(0px)`,
|
||||
transform: 'translateY(0px)',
|
||||
zIndex: 1
|
||||
}))
|
||||
|
||||
@ -312,12 +312,12 @@ const setItemRef = (el, index) => {
|
||||
|
||||
const measureItem = (index, element) => {
|
||||
if (!element) return
|
||||
|
||||
|
||||
const firstChild = element.firstElementChild
|
||||
const targetElement = firstChild || element
|
||||
|
||||
|
||||
const height = Math.ceil(targetElement.offsetHeight)
|
||||
|
||||
|
||||
if (height > 0) {
|
||||
const cachedHeight = itemHeights.value.get(index)
|
||||
if (cachedHeight !== height) {
|
||||
@ -332,10 +332,10 @@ const setupResizeObserver = () => {
|
||||
if (resizeObserver.value) {
|
||||
resizeObserver.value.disconnect()
|
||||
}
|
||||
|
||||
|
||||
resizeObserver.value = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
const index = parseInt(entry.target.dataset.index, 10)
|
||||
const index = Number.parseInt(entry.target.dataset.index, 10)
|
||||
if (!isNaN(index)) {
|
||||
measureItem(index, entry.target)
|
||||
}
|
||||
@ -345,15 +345,15 @@ const setupResizeObserver = () => {
|
||||
|
||||
const handleWheel = (event) => {
|
||||
if (!renderContainerRef.value) return
|
||||
|
||||
|
||||
const { deltaY } = event
|
||||
const el = renderContainerRef.value
|
||||
|
||||
|
||||
el.scrollBy({
|
||||
top: -deltaY,
|
||||
behavior: 'instant'
|
||||
})
|
||||
|
||||
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
@ -363,15 +363,15 @@ const handleScroll = (event) => {
|
||||
const target = event.target
|
||||
scrollTop.value = target.scrollTop
|
||||
isScrolling.value = true
|
||||
|
||||
|
||||
if (scrollTimeout.value) {
|
||||
clearTimeout(scrollTimeout.value)
|
||||
}
|
||||
|
||||
|
||||
scrollTimeout.value = setTimeout(() => {
|
||||
isScrolling.value = false
|
||||
}, 150)
|
||||
|
||||
|
||||
// 滚动时添加防抖清理,每100ms最多执行一次
|
||||
if (scrollCleanupTimeout.value) {
|
||||
clearTimeout(scrollCleanupTimeout.value)
|
||||
@ -379,19 +379,19 @@ const handleScroll = (event) => {
|
||||
scrollCleanupTimeout.value = setTimeout(() => {
|
||||
cleanupExtraItems(visibleItems.value)
|
||||
}, 300)
|
||||
|
||||
|
||||
const st = target.scrollTop
|
||||
const scrollHeight = target.scrollHeight
|
||||
const clientHeight = target.clientHeight
|
||||
|
||||
|
||||
const distanceToContainerTop = st
|
||||
const distanceToContainerBottom = scrollHeight - st - clientHeight
|
||||
|
||||
|
||||
const distanceToPageTop = distanceToContainerBottom
|
||||
const distanceToPageBottom = distanceToContainerTop
|
||||
const isAtPageTop = distanceToPageTop <= 0
|
||||
const isAtPageBottom = distanceToPageBottom <= 0
|
||||
|
||||
|
||||
emit('scroll', {
|
||||
target,
|
||||
scrollTop: st,
|
||||
@ -402,11 +402,11 @@ const handleScroll = (event) => {
|
||||
isAtPageTop,
|
||||
isAtPageBottom
|
||||
})
|
||||
|
||||
|
||||
if (isAtPageTop) {
|
||||
emit('scroll-start')
|
||||
}
|
||||
|
||||
|
||||
if (isAtPageBottom) {
|
||||
emit('scroll-end')
|
||||
}
|
||||
@ -414,9 +414,9 @@ const handleScroll = (event) => {
|
||||
|
||||
const scrollToIndex = (index, behavior = 'auto') => {
|
||||
if (!renderContainerRef.value || index < 0 || index >= computedData.value.length) return
|
||||
|
||||
|
||||
const position = getItemPosition(index)
|
||||
|
||||
|
||||
renderContainerRef.value.scrollTo({
|
||||
top: position.offset,
|
||||
behavior
|
||||
@ -428,10 +428,10 @@ const scrollToBottom = (behavior = 'smooth') => {
|
||||
pendingScrollToBottom.value = true
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
if (!renderContainerRef.value) return
|
||||
|
||||
|
||||
renderContainerRef.value.scrollTo({
|
||||
top: 0,
|
||||
behavior
|
||||
@ -441,10 +441,10 @@ const scrollToBottom = (behavior = 'smooth') => {
|
||||
|
||||
const scrollToTop = (behavior = 'smooth') => {
|
||||
if (!renderContainerRef.value) return
|
||||
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
if (!renderContainerRef.value) return
|
||||
|
||||
|
||||
const scrollHeight = renderContainerRef.value.scrollHeight
|
||||
renderContainerRef.value.scrollTo({
|
||||
top: scrollHeight,
|
||||
@ -479,9 +479,9 @@ const isAtPageTop = () => {
|
||||
|
||||
const observeVisibleItems = () => {
|
||||
if (!resizeObserver.value) return
|
||||
|
||||
|
||||
resizeObserver.value.disconnect()
|
||||
|
||||
|
||||
for (const [index, element] of itemRefs) {
|
||||
if (element) {
|
||||
resizeObserver.value.observe(element)
|
||||
@ -492,24 +492,24 @@ const observeVisibleItems = () => {
|
||||
watch(() => computedData.value, (newData, oldData) => {
|
||||
const oldLength = oldData?.length || 0
|
||||
const newLength = newData.length
|
||||
|
||||
|
||||
if (newLength !== oldLength) {
|
||||
const newHeights = new Map()
|
||||
|
||||
|
||||
const minLen = Math.min(oldLength, newLength)
|
||||
for (let i = 0; i < minLen; i++) {
|
||||
if (itemHeights.value.has(i)) {
|
||||
newHeights.set(i, itemHeights.value.get(i))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
itemHeights.value = newHeights
|
||||
|
||||
|
||||
nextTick(() => {
|
||||
observeVisibleItems()
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
previousDataLength.value = newLength
|
||||
}, { deep: false })
|
||||
|
||||
@ -527,36 +527,36 @@ watch(visibleItems, (newItems) => {
|
||||
|
||||
const cleanupExtraItems = (currentVisibleItems) => {
|
||||
if (!renderContainerRef.value || !currentVisibleItems.length) return
|
||||
|
||||
|
||||
// 构建当前应该可见的索引集合
|
||||
const visibleIndices = new Set(currentVisibleItems.map(item => item.index))
|
||||
|
||||
const visibleIndices = new Set(currentVisibleItems.map((item) => item.index))
|
||||
|
||||
// 直接获取 render-container 内所有实际渲染的 .virtual-scroller-item 元素
|
||||
const renderedItems = renderContainerRef.value.querySelectorAll('.virtual-scroller-item')
|
||||
|
||||
|
||||
const toRemove = []
|
||||
|
||||
|
||||
for (const el of renderedItems) {
|
||||
const dataIndex = parseInt(el.getAttribute('data-index'), 10)
|
||||
|
||||
const dataIndex = Number.parseInt(el.getAttribute('data-index'), 10)
|
||||
|
||||
// 如果元素的 data-index 不在可见范围内,标记为删除
|
||||
if (!isNaN(dataIndex) && !visibleIndices.has(dataIndex)) {
|
||||
toRemove.push(el)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 从 DOM 中删除多余元素
|
||||
for (const el of toRemove) {
|
||||
if (el.parentNode) {
|
||||
el.parentNode.removeChild(el)
|
||||
}
|
||||
// 同步清理 itemRefs Map
|
||||
const index = parseInt(el.getAttribute('data-index'), 10)
|
||||
const index = Number.parseInt(el.getAttribute('data-index'), 10)
|
||||
if (!isNaN(index)) {
|
||||
itemRefs.delete(index)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (toRemove.length > 0) {
|
||||
console.log(`[VirtualScroller] 清理了 ${toRemove.length} 个多余DOM元素`)
|
||||
}
|
||||
@ -566,13 +566,13 @@ onMounted(() => {
|
||||
setupResizeObserver()
|
||||
isInitialized.value = true
|
||||
previousDataLength.value = computedData.value.length
|
||||
|
||||
|
||||
nextTick(() => {
|
||||
if (pendingScrollToBottom.value) {
|
||||
pendingScrollToBottom.value = false
|
||||
scrollToBottom()
|
||||
}
|
||||
|
||||
|
||||
observeVisibleItems()
|
||||
cleanupExtraItems(visibleItems.value)
|
||||
})
|
||||
@ -582,15 +582,15 @@ onBeforeUnmount(() => {
|
||||
if (resizeObserver.value) {
|
||||
resizeObserver.value.disconnect()
|
||||
}
|
||||
|
||||
|
||||
if (scrollTimeout.value) {
|
||||
clearTimeout(scrollTimeout.value)
|
||||
}
|
||||
|
||||
|
||||
if (scrollCleanupTimeout.value) {
|
||||
clearTimeout(scrollCleanupTimeout.value)
|
||||
}
|
||||
|
||||
|
||||
itemRefs.clear()
|
||||
})
|
||||
|
||||
@ -611,56 +611,56 @@ defineExpose({
|
||||
<style lang="less" scoped>
|
||||
.virtual-scroller {
|
||||
-webkit-overflow-scrolling: touch;
|
||||
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 3px;
|
||||
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.virtual-scroller-wrapper {
|
||||
contain: content;
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
.virtual-scroller-spacer {
|
||||
flex-shrink: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
.virtual-scroller-placeholder {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
.virtual-scroller-render-container {
|
||||
contain: layout style;
|
||||
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.virtual-scroller-item {
|
||||
contain: layout style;
|
||||
backface-visibility: hidden;
|
||||
perspective: 1000px;
|
||||
}
|
||||
|
||||
|
||||
.virtual-scroller-bottom-placeholder {
|
||||
contain: layout style;
|
||||
}
|
||||
|
||||
@ -99,6 +99,8 @@ const props = defineProps({
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['scroll', 'scroll-start', 'scroll-end', 'visible-change'])
|
||||
|
||||
const computedData = computed(() => {
|
||||
return props.data.length > 0 ? props.data : props.items
|
||||
})
|
||||
@ -113,8 +115,6 @@ const computedBuffer = computed(() => {
|
||||
return props.buffer !== 3 ? props.buffer : props.bufferSize
|
||||
})
|
||||
|
||||
const emit = defineEmits(['scroll', 'scroll-start', 'scroll-end', 'visible-change'])
|
||||
|
||||
const containerRef = ref(null)
|
||||
const wrapperRef = ref(null)
|
||||
const renderContainerRef = ref(null)
|
||||
@ -136,27 +136,27 @@ const totalHeight = computed(() => {
|
||||
heightVersion.value // 依赖追踪:measureItem 直接 mutate Map 时通过此版本号触发重算
|
||||
let height = 0
|
||||
const len = computedData.value.length
|
||||
|
||||
|
||||
for (let i = 0; i < len; i++) {
|
||||
const cachedHeight = itemHeights.value.get(i)
|
||||
height += cachedHeight !== undefined ? cachedHeight : props.estimatedHeight
|
||||
}
|
||||
|
||||
|
||||
height += props.bottomPlaceholderHeight
|
||||
|
||||
|
||||
return height
|
||||
})
|
||||
|
||||
const getItemPosition = (index) => {
|
||||
let offset = 0
|
||||
|
||||
|
||||
for (let i = 0; i < index; i++) {
|
||||
const cachedHeight = itemHeights.value.get(i)
|
||||
offset += cachedHeight !== undefined ? cachedHeight : props.estimatedHeight
|
||||
}
|
||||
|
||||
|
||||
const height = itemHeights.value.get(index) ?? props.estimatedHeight
|
||||
|
||||
|
||||
return { offset, height }
|
||||
}
|
||||
|
||||
@ -218,23 +218,23 @@ const visibleRange = computed(() => {
|
||||
const visibleItems = computed(() => {
|
||||
const { start, end, offset } = visibleRange.value
|
||||
const items = []
|
||||
|
||||
|
||||
let currentOffset = offset
|
||||
|
||||
|
||||
for (let i = start; i <= end && i < computedData.value.length; i++) {
|
||||
const cachedHeight = itemHeights.value.get(i)
|
||||
const height = cachedHeight !== undefined ? cachedHeight : props.estimatedHeight
|
||||
|
||||
|
||||
items.push({
|
||||
item: computedData.value[i],
|
||||
index: i,
|
||||
offset: currentOffset + props.bottomPlaceholderHeight,
|
||||
height
|
||||
})
|
||||
|
||||
|
||||
currentOffset += height
|
||||
}
|
||||
|
||||
|
||||
return items
|
||||
})
|
||||
|
||||
@ -269,7 +269,7 @@ const bottomPlaceholderStyle = computed(() => ({
|
||||
top: 0,
|
||||
width: '100%',
|
||||
height: `${props.bottomPlaceholderHeight}px`,
|
||||
transform: `translateY(0px)`,
|
||||
transform: 'translateY(0px)',
|
||||
zIndex: 1
|
||||
}))
|
||||
|
||||
@ -344,10 +344,10 @@ const setupResizeObserver = () => {
|
||||
if (resizeObserver.value) {
|
||||
resizeObserver.value.disconnect()
|
||||
}
|
||||
|
||||
|
||||
resizeObserver.value = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
const index = parseInt(entry.target.dataset.index, 10)
|
||||
const index = Number.parseInt(entry.target.dataset.index, 10)
|
||||
if (!isNaN(index)) {
|
||||
measureItem(index, entry.target)
|
||||
}
|
||||
@ -357,15 +357,15 @@ const setupResizeObserver = () => {
|
||||
|
||||
const handleWheel = (event) => {
|
||||
if (!renderContainerRef.value) return
|
||||
|
||||
|
||||
const { deltaY } = event
|
||||
const el = renderContainerRef.value
|
||||
|
||||
|
||||
el.scrollBy({
|
||||
top: -deltaY,
|
||||
behavior: 'instant'
|
||||
})
|
||||
|
||||
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
@ -376,15 +376,15 @@ const handleScroll = (event) => {
|
||||
const st = target.scrollTop
|
||||
const scrollHeight = target.scrollHeight
|
||||
const clientHeight = target.clientHeight
|
||||
|
||||
|
||||
const distanceToContainerTop = st
|
||||
const distanceToContainerBottom = scrollHeight - st - clientHeight
|
||||
|
||||
|
||||
const distanceToPageTop = distanceToContainerBottom
|
||||
const distanceToPageBottom = distanceToContainerTop
|
||||
const isAtPageTop = distanceToPageTop <= 0
|
||||
const isAtPageBottom = distanceToPageBottom <= 0
|
||||
|
||||
|
||||
emit('scroll', {
|
||||
target,
|
||||
scrollTop: st,
|
||||
@ -395,11 +395,11 @@ const handleScroll = (event) => {
|
||||
isAtPageTop,
|
||||
isAtPageBottom
|
||||
})
|
||||
|
||||
|
||||
if (isAtPageTop) {
|
||||
emit('scroll-start')
|
||||
}
|
||||
|
||||
|
||||
if (isAtPageBottom) {
|
||||
emit('scroll-end')
|
||||
}
|
||||
@ -407,9 +407,9 @@ const handleScroll = (event) => {
|
||||
|
||||
const scrollToIndex = (index, behavior = 'auto') => {
|
||||
if (!renderContainerRef.value || index < 0 || index >= computedData.value.length) return
|
||||
|
||||
|
||||
const position = getItemPosition(index)
|
||||
|
||||
|
||||
renderContainerRef.value.scrollTo({
|
||||
top: position.offset,
|
||||
behavior
|
||||
@ -421,10 +421,10 @@ const scrollToBottom = (behavior = 'smooth') => {
|
||||
pendingScrollToBottom.value = true
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
if (!renderContainerRef.value) return
|
||||
|
||||
|
||||
renderContainerRef.value.scrollTo({
|
||||
top: 0,
|
||||
behavior
|
||||
@ -434,10 +434,10 @@ const scrollToBottom = (behavior = 'smooth') => {
|
||||
|
||||
const scrollToTop = (behavior = 'smooth') => {
|
||||
if (!renderContainerRef.value) return
|
||||
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
if (!renderContainerRef.value) return
|
||||
|
||||
|
||||
const scrollHeight = renderContainerRef.value.scrollHeight
|
||||
renderContainerRef.value.scrollTo({
|
||||
top: scrollHeight,
|
||||
@ -552,56 +552,56 @@ defineExpose({
|
||||
<style lang="less" scoped>
|
||||
.virtual-scroller {
|
||||
-webkit-overflow-scrolling: touch;
|
||||
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 3px;
|
||||
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.virtual-scroller-wrapper {
|
||||
contain: content;
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
.virtual-scroller-spacer {
|
||||
flex-shrink: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
.virtual-scroller-placeholder {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
.virtual-scroller-render-container {
|
||||
contain: layout style;
|
||||
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.virtual-scroller-item {
|
||||
contain: layout style;
|
||||
backface-visibility: hidden;
|
||||
perspective: 1000px;
|
||||
}
|
||||
|
||||
|
||||
.virtual-scroller-bottom-placeholder {
|
||||
contain: layout style;
|
||||
}
|
||||
|
||||
@ -1,194 +1,194 @@
|
||||
window.TEST_DATA = [
|
||||
{
|
||||
"id": "839217090555557410",
|
||||
"fileUrl": "http://test.xueai.art/file/2026/4/27/69ef43d23888d39e4ed1d062.png",
|
||||
"prompt": "这是一幅治愈写实风格的插画,画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶,空调外机挂在墙边,金属防护栏环绕四周,外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅,旁边还有小凳。彩色水桶,一个是红色,一个是蓝色,放置在角落,花盆里栽种着金桔,水仙花、蝴蝶兰,整齐排列,大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树,绿意浓郁,与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼,整个场景被绿意环绕,明亮柔和的光线洒下,充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格",
|
||||
"model": "flux"
|
||||
id: '839217090555557410',
|
||||
fileUrl: 'http://test.xueai.art/file/2026/4/27/69ef43d23888d39e4ed1d062.png',
|
||||
prompt: '这是一幅治愈写实风格的插画,画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶,空调外机挂在墙边,金属防护栏环绕四周,外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅,旁边还有小凳。彩色水桶,一个是红色,一个是蓝色,放置在角落,花盆里栽种着金桔,水仙花、蝴蝶兰,整齐排列,大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树,绿意浓郁,与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼,整个场景被绿意环绕,明亮柔和的光线洒下,充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格',
|
||||
model: 'flux'
|
||||
},
|
||||
{
|
||||
"id": "839211834861958673",
|
||||
"fileUrl": "http://test.xueai.art/file/2026/4/27/69ef3eed3888d39e4ed1d061.png",
|
||||
"prompt": "这是一幅治愈写实风格的插画,画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶,空调外机挂在墙边,金属防护栏环绕四周,外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅,旁边还有小凳。彩色水桶,一个是红色,一个是蓝色,放置在角落,花盆里栽种着金桔,水仙花、蝴蝶兰,整齐排列,大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树,绿意浓郁,与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼,整个场景被绿意环绕,明亮柔和的光线洒下,充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格",
|
||||
"model": "BananaPro"
|
||||
id: '839211834861958673',
|
||||
fileUrl: 'http://test.xueai.art/file/2026/4/27/69ef3eed3888d39e4ed1d061.png',
|
||||
prompt: '这是一幅治愈写实风格的插画,画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶,空调外机挂在墙边,金属防护栏环绕四周,外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅,旁边还有小凳。彩色水桶,一个是红色,一个是蓝色,放置在角落,花盆里栽种着金桔,水仙花、蝴蝶兰,整齐排列,大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树,绿意浓郁,与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼,整个场景被绿意环绕,明亮柔和的光线洒下,充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格',
|
||||
model: 'BananaPro'
|
||||
},
|
||||
{
|
||||
"id": "839209605929121287",
|
||||
"fileUrl": "http://test.xueai.art/file/2026/4/27/69ef3cd93888d39e4ed1d060.png",
|
||||
"prompt": "这是一幅治愈写实风格的插画,画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶,空调外机挂在墙边,金属防护栏环绕四周,外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅,旁边还有小凳。彩色水桶,一个是红色,一个是蓝色,放置在角落,花盆里栽种着金桔,水仙花、蝴蝶兰,整齐排列,大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树,绿意浓郁,与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼,整个场景被绿意环绕,明亮柔和的光线洒下,充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格",
|
||||
"model": "flux"
|
||||
id: '839209605929121287',
|
||||
fileUrl: 'http://test.xueai.art/file/2026/4/27/69ef3cd93888d39e4ed1d060.png',
|
||||
prompt: '这是一幅治愈写实风格的插画,画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶,空调外机挂在墙边,金属防护栏环绕四周,外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅,旁边还有小凳。彩色水桶,一个是红色,一个是蓝色,放置在角落,花盆里栽种着金桔,水仙花、蝴蝶兰,整齐排列,大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树,绿意浓郁,与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼,整个场景被绿意环绕,明亮柔和的光线洒下,充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格',
|
||||
model: 'flux'
|
||||
},
|
||||
{
|
||||
"id": "837370053866304564",
|
||||
"fileUrl": "http://test.xueai.art/file/2026/4/22/69e88ba23888d39e4ed1d05c.png",
|
||||
"prompt": "这是一幅治愈写实风格的插画,画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶,空调外机挂在墙边,金属防护栏环绕四周,外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅,旁边还有小凳。彩色水桶,一个是红色,一个是蓝色,放置在角落,花盆里栽种着金桔,水仙花、蝴蝶兰,整齐排列,大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树,绿意浓郁,与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼,整个场景被绿意环绕,明亮柔和的光线洒下,充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格",
|
||||
"model": "flux"
|
||||
id: '837370053866304564',
|
||||
fileUrl: 'http://test.xueai.art/file/2026/4/22/69e88ba23888d39e4ed1d05c.png',
|
||||
prompt: '这是一幅治愈写实风格的插画,画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶,空调外机挂在墙边,金属防护栏环绕四周,外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅,旁边还有小凳。彩色水桶,一个是红色,一个是蓝色,放置在角落,花盆里栽种着金桔,水仙花、蝴蝶兰,整齐排列,大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树,绿意浓郁,与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼,整个场景被绿意环绕,明亮柔和的光线洒下,充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格',
|
||||
model: 'flux'
|
||||
},
|
||||
{
|
||||
"id": "837360015437214709",
|
||||
"fileUrl": "http://test.xueai.art/file/2026/4/22/69e882493888d39e4ed1d05b.png",
|
||||
"prompt": "这是一幅治愈写实风格的插画,画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶,空调外机挂在墙边,金属防护栏环绕四周,外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅,旁边还有小凳。彩色水桶,一个是红色,一个是蓝色,放置在角落,花盆里栽种着金桔,水仙花、蝴蝶兰,整齐排列,大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树,绿意浓郁,与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼,整个场景被绿意环绕,明亮柔和的光线洒下,充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格",
|
||||
"model": "flux"
|
||||
id: '837360015437214709',
|
||||
fileUrl: 'http://test.xueai.art/file/2026/4/22/69e882493888d39e4ed1d05b.png',
|
||||
prompt: '这是一幅治愈写实风格的插画,画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶,空调外机挂在墙边,金属防护栏环绕四周,外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅,旁边还有小凳。彩色水桶,一个是红色,一个是蓝色,放置在角落,花盆里栽种着金桔,水仙花、蝴蝶兰,整齐排列,大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树,绿意浓郁,与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼,整个场景被绿意环绕,明亮柔和的光线洒下,充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格',
|
||||
model: 'flux'
|
||||
},
|
||||
{
|
||||
"id": "836534979084169461",
|
||||
"fileUrl": "http://test.xueai.art/file/2026/4/20/69e581e93888d39e4ed1d025.png",
|
||||
"prompt": "<div data-v-43afc57f=\"\" class=\"prompt-container\" style=\"width: 1118px; font-size: 14px; background-color: rgb(255, 255, 255); height: 39px;\"><div data-v-43afc57f=\"\" class=\"prompt-wrapper\" style=\"width: 1118px;\"><div data-v-43afc57f=\"\" class=\"prompt expanded\" style=\"max-height: none; overflow: visible;\"><span data-v-43afc57f=\"\" class=\"prompt-text\">将图1红色框内的【苹果】替换为【火龙果】</span></div><div><span data-v-43afc57f=\"\" class=\"prompt-text\"><br></span></div></div></div><div data-v-43afc57f=\"\" class=\"box success-box\" style=\"width: 1118px; font-size: 14px; background-color: rgb(255, 255, 255);\"><div data-v-43afc57f=\"\" class=\"one-box\"></div></div>",
|
||||
"model": "banana"
|
||||
id: '836534979084169461',
|
||||
fileUrl: 'http://test.xueai.art/file/2026/4/20/69e581e93888d39e4ed1d025.png',
|
||||
prompt: '<div data-v-43afc57f="" class="prompt-container" style="width: 1118px; font-size: 14px; background-color: rgb(255, 255, 255); height: 39px;"><div data-v-43afc57f="" class="prompt-wrapper" style="width: 1118px;"><div data-v-43afc57f="" class="prompt expanded" style="max-height: none; overflow: visible;"><span data-v-43afc57f="" class="prompt-text">将图1红色框内的【苹果】替换为【火龙果】</span></div><div><span data-v-43afc57f="" class="prompt-text"><br></span></div></div></div><div data-v-43afc57f="" class="box success-box" style="width: 1118px; font-size: 14px; background-color: rgb(255, 255, 255);"><div data-v-43afc57f="" class="one-box"></div></div>',
|
||||
model: 'banana'
|
||||
},
|
||||
{
|
||||
"id": "835464458670191734",
|
||||
"fileUrl": "http://test.xueai.art/file/2026/4/17/69e19ce93888d39e4ed1cfaf.png",
|
||||
"prompt": "这是一幅治愈写实风格的插画,画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶,空调外机挂在墙边,金属防护栏环绕四周,外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅,旁边还有小凳。彩色水桶,一个是红色,一个是蓝色,放置在角落,花盆里栽种着金桔,水仙花、蝴蝶兰,整齐排列,大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树,绿意浓郁,与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼,整个场景被绿意环绕,明亮柔和的光线洒下,充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格",
|
||||
"model": "flux"
|
||||
id: '835464458670191734',
|
||||
fileUrl: 'http://test.xueai.art/file/2026/4/17/69e19ce93888d39e4ed1cfaf.png',
|
||||
prompt: '这是一幅治愈写实风格的插画,画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶,空调外机挂在墙边,金属防护栏环绕四周,外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅,旁边还有小凳。彩色水桶,一个是红色,一个是蓝色,放置在角落,花盆里栽种着金桔,水仙花、蝴蝶兰,整齐排列,大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树,绿意浓郁,与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼,整个场景被绿意环绕,明亮柔和的光线洒下,充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格',
|
||||
model: 'flux'
|
||||
},
|
||||
{
|
||||
"id": "835463648116749398",
|
||||
"fileUrl": "http://test.xueai.art/file/2026/4/17/69e19c273888d39e4ed1cfac.png",
|
||||
"prompt": "这是一幅治愈写实风格的插画,画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶,空调外机挂在墙边,金属防护栏环绕四周,外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅,旁边还有小凳。彩色水桶,一个是红色,一个是蓝色,放置在角落,花盆里栽种着金桔,水仙花、蝴蝶兰,整齐排列,大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树,绿意浓郁,与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼,整个场景被绿意环绕,明亮柔和的光线洒下,充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格",
|
||||
"model": "flux"
|
||||
id: '835463648116749398',
|
||||
fileUrl: 'http://test.xueai.art/file/2026/4/17/69e19c273888d39e4ed1cfac.png',
|
||||
prompt: '这是一幅治愈写实风格的插画,画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶,空调外机挂在墙边,金属防护栏环绕四周,外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅,旁边还有小凳。彩色水桶,一个是红色,一个是蓝色,放置在角落,花盆里栽种着金桔,水仙花、蝴蝶兰,整齐排列,大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树,绿意浓郁,与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼,整个场景被绿意环绕,明亮柔和的光线洒下,充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格',
|
||||
model: 'flux'
|
||||
},
|
||||
{
|
||||
"id": "835463392293565520",
|
||||
"fileUrl": "http://test.xueai.art/file/2026/4/17/69e19bea3888d39e4ed1cfab.png",
|
||||
"prompt": "这是一幅治愈写实风格的插画,画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶,空调外机挂在墙边,金属防护栏环绕四周,外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅,旁边还有小凳。彩色水桶,一个是红色,一个是蓝色,放置在角落,花盆里栽种着金桔,水仙花、蝴蝶兰,整齐排列,大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树,绿意浓郁,与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼,整个场景被绿意环绕,明亮柔和的光线洒下,充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格",
|
||||
"model": "flux"
|
||||
id: '835463392293565520',
|
||||
fileUrl: 'http://test.xueai.art/file/2026/4/17/69e19bea3888d39e4ed1cfab.png',
|
||||
prompt: '这是一幅治愈写实风格的插画,画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶,空调外机挂在墙边,金属防护栏环绕四周,外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅,旁边还有小凳。彩色水桶,一个是红色,一个是蓝色,放置在角落,花盆里栽种着金桔,水仙花、蝴蝶兰,整齐排列,大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树,绿意浓郁,与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼,整个场景被绿意环绕,明亮柔和的光线洒下,充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格',
|
||||
model: 'flux'
|
||||
},
|
||||
{
|
||||
"id": "832562717234575283",
|
||||
"fileUrl": "http://test.xueai.art/file/2026/4/9/69d70e733888d39e4ed1cf66.png,http://test.xueai.art/file/2026/4/9/69d70e733888d39e4ed1cf67.png,http://test.xueai.art/file/2026/4/9/69d70e733888d39e4ed1cf68.png,http://test.xueai.art/file/2026/4/9/69d70e743888d39e4ed1cf69.png",
|
||||
"prompt": "这是一幅治愈写实风格的插画,画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶,空调外机挂在墙边,金属防护栏环绕四周,外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅,旁边还有小凳。彩色水桶,一个是红色,一个是蓝色,放置在角落,花盆里栽种着金桔,水仙花、蝴蝶兰,整齐排列,大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树,绿意浓郁,与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼,整个场景被绿意环绕,明亮柔和的光线洒下,充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格",
|
||||
"model": "flux"
|
||||
id: '832562717234575283',
|
||||
fileUrl: 'http://test.xueai.art/file/2026/4/9/69d70e733888d39e4ed1cf66.png,http://test.xueai.art/file/2026/4/9/69d70e733888d39e4ed1cf67.png,http://test.xueai.art/file/2026/4/9/69d70e733888d39e4ed1cf68.png,http://test.xueai.art/file/2026/4/9/69d70e743888d39e4ed1cf69.png',
|
||||
prompt: '这是一幅治愈写实风格的插画,画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶,空调外机挂在墙边,金属防护栏环绕四周,外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅,旁边还有小凳。彩色水桶,一个是红色,一个是蓝色,放置在角落,花盆里栽种着金桔,水仙花、蝴蝶兰,整齐排列,大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树,绿意浓郁,与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼,整个场景被绿意环绕,明亮柔和的光线洒下,充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格',
|
||||
model: 'flux'
|
||||
},
|
||||
{
|
||||
"id": "839217090555557410",
|
||||
"fileUrl": "http://test.xueai.art/file/2026/4/27/69ef43d23888d39e4ed1d062.png",
|
||||
"prompt": "这是一幅治愈写实风格的插画,画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶,空调外机挂在墙边,金属防护栏环绕四周,外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅,旁边还有小凳。彩色水桶,一个是红色,一个是蓝色,放置在角落,花盆里栽种着金桔,水仙花、蝴蝶兰,整齐排列,大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树,绿意浓郁,与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼,整个场景被绿意环绕,明亮柔和的光线洒下,充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格",
|
||||
"model": "flux"
|
||||
id: '839217090555557410',
|
||||
fileUrl: 'http://test.xueai.art/file/2026/4/27/69ef43d23888d39e4ed1d062.png',
|
||||
prompt: '这是一幅治愈写实风格的插画,画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶,空调外机挂在墙边,金属防护栏环绕四周,外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅,旁边还有小凳。彩色水桶,一个是红色,一个是蓝色,放置在角落,花盆里栽种着金桔,水仙花、蝴蝶兰,整齐排列,大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树,绿意浓郁,与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼,整个场景被绿意环绕,明亮柔和的光线洒下,充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格',
|
||||
model: 'flux'
|
||||
},
|
||||
{
|
||||
"id": "839211834861958673",
|
||||
"fileUrl": "http://test.xueai.art/file/2026/4/27/69ef3eed3888d39e4ed1d061.png",
|
||||
"prompt": "这是一幅治愈写实风格的插画,画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶,空调外机挂在墙边,金属防护栏环绕四周,外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅,旁边还有小凳。彩色水桶,一个是红色,一个是蓝色,放置在角落,花盆里栽种着金桔,水仙花、蝴蝶兰,整齐排列,大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树,绿意浓郁,与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼,整个场景被绿意环绕,明亮柔和的光线洒下,充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格",
|
||||
"model": "BananaPro"
|
||||
id: '839211834861958673',
|
||||
fileUrl: 'http://test.xueai.art/file/2026/4/27/69ef3eed3888d39e4ed1d061.png',
|
||||
prompt: '这是一幅治愈写实风格的插画,画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶,空调外机挂在墙边,金属防护栏环绕四周,外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅,旁边还有小凳。彩色水桶,一个是红色,一个是蓝色,放置在角落,花盆里栽种着金桔,水仙花、蝴蝶兰,整齐排列,大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树,绿意浓郁,与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼,整个场景被绿意环绕,明亮柔和的光线洒下,充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格',
|
||||
model: 'BananaPro'
|
||||
},
|
||||
{
|
||||
"id": "839209605929121287",
|
||||
"fileUrl": "http://test.xueai.art/file/2026/4/27/69ef3cd93888d39e4ed1d060.png",
|
||||
"prompt": "这是一幅治愈写实风格的插画,画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶,空调外机挂在墙边,金属防护栏环绕四周,外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅,旁边还有小凳。彩色水桶,一个是红色,一个是蓝色,放置在角落,花盆里栽种着金桔,水仙花、蝴蝶兰,整齐排列,大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树,绿意浓郁,与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼,整个场景被绿意环绕,明亮柔和的光线洒下,充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格",
|
||||
"model": "flux"
|
||||
id: '839209605929121287',
|
||||
fileUrl: 'http://test.xueai.art/file/2026/4/27/69ef3cd93888d39e4ed1d060.png',
|
||||
prompt: '这是一幅治愈写实风格的插画,画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶,空调外机挂在墙边,金属防护栏环绕四周,外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅,旁边还有小凳。彩色水桶,一个是红色,一个是蓝色,放置在角落,花盆里栽种着金桔,水仙花、蝴蝶兰,整齐排列,大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树,绿意浓郁,与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼,整个场景被绿意环绕,明亮柔和的光线洒下,充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格',
|
||||
model: 'flux'
|
||||
},
|
||||
{
|
||||
"id": "837370053866304564",
|
||||
"fileUrl": "http://test.xueai.art/file/2026/4/22/69e88ba23888d39e4ed1d05c.png",
|
||||
"prompt": "这是一幅治愈写实风格的插画,画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶,空调外机挂在墙边,金属防护栏环绕四周,外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅,旁边还有小凳。彩色水桶,一个是红色,一个是蓝色,放置在角落,花盆里栽种着金桔,水仙花、蝴蝶兰,整齐排列,大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树,绿意浓郁,与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼,整个场景被绿意环绕,明亮柔和的光线洒下,充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格",
|
||||
"model": "flux"
|
||||
id: '837370053866304564',
|
||||
fileUrl: 'http://test.xueai.art/file/2026/4/22/69e88ba23888d39e4ed1d05c.png',
|
||||
prompt: '这是一幅治愈写实风格的插画,画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶,空调外机挂在墙边,金属防护栏环绕四周,外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅,旁边还有小凳。彩色水桶,一个是红色,一个是蓝色,放置在角落,花盆里栽种着金桔,水仙花、蝴蝶兰,整齐排列,大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树,绿意浓郁,与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼,整个场景被绿意环绕,明亮柔和的光线洒下,充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格',
|
||||
model: 'flux'
|
||||
},
|
||||
{
|
||||
"id": "837360015437214709",
|
||||
"fileUrl": "http://test.xueai.art/file/2026/4/22/69e882493888d39e4ed1d05b.png",
|
||||
"prompt": "这是一幅治愈写实风格的插画,画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶,空调外机挂在墙边,金属防护栏环绕四周,外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅,旁边还有小凳。彩色水桶,一个是红色,一个是蓝色,放置在角落,花盆里栽种着金桔,水仙花、蝴蝶兰,整齐排列,大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树,绿意浓郁,与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼,整个场景被绿意环绕,明亮柔和的光线洒下,充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格",
|
||||
"model": "flux"
|
||||
id: '837360015437214709',
|
||||
fileUrl: 'http://test.xueai.art/file/2026/4/22/69e882493888d39e4ed1d05b.png',
|
||||
prompt: '这是一幅治愈写实风格的插画,画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶,空调外机挂在墙边,金属防护栏环绕四周,外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅,旁边还有小凳。彩色水桶,一个是红色,一个是蓝色,放置在角落,花盆里栽种着金桔,水仙花、蝴蝶兰,整齐排列,大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树,绿意浓郁,与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼,整个场景被绿意环绕,明亮柔和的光线洒下,充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格',
|
||||
model: 'flux'
|
||||
},
|
||||
{
|
||||
"id": "836534979084169461",
|
||||
"fileUrl": "http://test.xueai.art/file/2026/4/20/69e581e93888d39e4ed1d025.png",
|
||||
"prompt": "<div data-v-43afc57f=\"\" class=\"prompt-container\" style=\"width: 1118px; font-size: 14px; background-color: rgb(255, 255, 255); height: 39px;\"><div data-v-43afc57f=\"\" class=\"prompt-wrapper\" style=\"width: 1118px;\"><div data-v-43afc57f=\"\" class=\"prompt expanded\" style=\"max-height: none; overflow: visible;\"><span data-v-43afc57f=\"\" class=\"prompt-text\">将图1红色框内的【苹果】替换为【火龙果】</span></div><div><span data-v-43afc57f=\"\" class=\"prompt-text\"><br></span></div></div></div><div data-v-43afc57f=\"\" class=\"box success-box\" style=\"width: 1118px; font-size: 14px; background-color: rgb(255, 255, 255);\"><div data-v-43afc57f=\"\" class=\"one-box\"></div></div>",
|
||||
"model": "banana"
|
||||
id: '836534979084169461',
|
||||
fileUrl: 'http://test.xueai.art/file/2026/4/20/69e581e93888d39e4ed1d025.png',
|
||||
prompt: '<div data-v-43afc57f="" class="prompt-container" style="width: 1118px; font-size: 14px; background-color: rgb(255, 255, 255); height: 39px;"><div data-v-43afc57f="" class="prompt-wrapper" style="width: 1118px;"><div data-v-43afc57f="" class="prompt expanded" style="max-height: none; overflow: visible;"><span data-v-43afc57f="" class="prompt-text">将图1红色框内的【苹果】替换为【火龙果】</span></div><div><span data-v-43afc57f="" class="prompt-text"><br></span></div></div></div><div data-v-43afc57f="" class="box success-box" style="width: 1118px; font-size: 14px; background-color: rgb(255, 255, 255);"><div data-v-43afc57f="" class="one-box"></div></div>',
|
||||
model: 'banana'
|
||||
},
|
||||
{
|
||||
"id": "835464458670191734",
|
||||
"fileUrl": "http://test.xueai.art/file/2026/4/17/69e19ce93888d39e4ed1cfaf.png",
|
||||
"prompt": "这是一幅治愈写实风格的插画,画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶,空调外机挂在墙边,金属防护栏环绕四周,外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅,旁边还有小凳。彩色水桶,一个是红色,一个是蓝色,放置在角落,花盆里栽种着金桔,水仙花、蝴蝶兰,整齐排列,大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树,绿意浓郁,与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼,整个场景被绿意环绕,明亮柔和的光线洒下,充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格",
|
||||
"model": "flux"
|
||||
id: '835464458670191734',
|
||||
fileUrl: 'http://test.xueai.art/file/2026/4/17/69e19ce93888d39e4ed1cfaf.png',
|
||||
prompt: '这是一幅治愈写实风格的插画,画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶,空调外机挂在墙边,金属防护栏环绕四周,外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅,旁边还有小凳。彩色水桶,一个是红色,一个是蓝色,放置在角落,花盆里栽种着金桔,水仙花、蝴蝶兰,整齐排列,大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树,绿意浓郁,与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼,整个场景被绿意环绕,明亮柔和的光线洒下,充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格',
|
||||
model: 'flux'
|
||||
},
|
||||
{
|
||||
"id": "835463648116749398",
|
||||
"fileUrl": "http://test.xueai.art/file/2026/4/17/69e19c273888d39e4ed1cfac.png",
|
||||
"prompt": "这是一幅治愈写实风格的插画,画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶,空调外机挂在墙边,金属防护栏环绕四周,外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅,旁边还有小凳。彩色水桶,一个是红色,一个是蓝色,放置在角落,花盆里栽种着金桔,水仙花、蝴蝶兰,整齐排列,大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树,绿意浓郁,与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼,整个场景被绿意环绕,明亮柔和的光线洒下,充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格",
|
||||
"model": "flux"
|
||||
id: '835463648116749398',
|
||||
fileUrl: 'http://test.xueai.art/file/2026/4/17/69e19c273888d39e4ed1cfac.png',
|
||||
prompt: '这是一幅治愈写实风格的插画,画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶,空调外机挂在墙边,金属防护栏环绕四周,外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅,旁边还有小凳。彩色水桶,一个是红色,一个是蓝色,放置在角落,花盆里栽种着金桔,水仙花、蝴蝶兰,整齐排列,大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树,绿意浓郁,与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼,整个场景被绿意环绕,明亮柔和的光线洒下,充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格',
|
||||
model: 'flux'
|
||||
},
|
||||
{
|
||||
"id": "835463392293565520",
|
||||
"fileUrl": "http://test.xueai.art/file/2026/4/17/69e19bea3888d39e4ed1cfab.png",
|
||||
"prompt": "这是一幅治愈写实风格的插画,画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶,空调外机挂在墙边,金属防护栏环绕四周,外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅,旁边还有小凳。彩色水桶,一个是红色,一个是蓝色,放置在角落,花盆里栽种着金桔,水仙花、蝴蝶兰,整齐排列,大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树,绿意浓郁,与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼,整个场景被绿意环绕,明亮柔和的光线洒下,充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格",
|
||||
"model": "flux"
|
||||
id: '835463392293565520',
|
||||
fileUrl: 'http://test.xueai.art/file/2026/4/17/69e19bea3888d39e4ed1cfab.png',
|
||||
prompt: '这是一幅治愈写实风格的插画,画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶,空调外机挂在墙边,金属防护栏环绕四周,外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅,旁边还有小凳。彩色水桶,一个是红色,一个是蓝色,放置在角落,花盆里栽种着金桔,水仙花、蝴蝶兰,整齐排列,大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树,绿意浓郁,与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼,整个场景被绿意环绕,明亮柔和的光线洒下,充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格',
|
||||
model: 'flux'
|
||||
},
|
||||
{
|
||||
"id": "832562717234575283",
|
||||
"fileUrl": "http://test.xueai.art/file/2026/4/9/69d70e733888d39e4ed1cf66.png,http://test.xueai.art/file/2026/4/9/69d70e733888d39e4ed1cf67.png,http://test.xueai.art/file/2026/4/9/69d70e733888d39e4ed1cf68.png,http://test.xueai.art/file/2026/4/9/69d70e743888d39e4ed1cf69.png",
|
||||
"prompt": "这是一幅治愈写实风格的插画,画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶,空调外机挂在墙边,金属防护栏环绕四周,外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅,旁边还有小凳。彩色水桶,一个是红色,一个是蓝色,放置在角落,花盆里栽种着金桔,水仙花、蝴蝶兰,整齐排列,大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树,绿意浓郁,与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼,整个场景被绿意环绕,明亮柔和的光线洒下,充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格",
|
||||
"model": "flux"
|
||||
id: '832562717234575283',
|
||||
fileUrl: 'http://test.xueai.art/file/2026/4/9/69d70e733888d39e4ed1cf66.png,http://test.xueai.art/file/2026/4/9/69d70e733888d39e4ed1cf67.png,http://test.xueai.art/file/2026/4/9/69d70e733888d39e4ed1cf68.png,http://test.xueai.art/file/2026/4/9/69d70e743888d39e4ed1cf69.png',
|
||||
prompt: '这是一幅治愈写实风格的插画,画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶,空调外机挂在墙边,金属防护栏环绕四周,外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅,旁边还有小凳。彩色水桶,一个是红色,一个是蓝色,放置在角落,花盆里栽种着金桔,水仙花、蝴蝶兰,整齐排列,大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树,绿意浓郁,与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼,整个场景被绿意环绕,明亮柔和的光线洒下,充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格',
|
||||
model: 'flux'
|
||||
},
|
||||
{
|
||||
"id": "830138209152283808",
|
||||
"fileUrl": "http://test.xueai.art/file/2026/4/2/69ce3c743888d39e4ed1cf4f.png",
|
||||
"prompt": "将图1红色框内的【苹果】替换为【火龙果】",
|
||||
"model": "banana"
|
||||
id: '830138209152283808',
|
||||
fileUrl: 'http://test.xueai.art/file/2026/4/2/69ce3c743888d39e4ed1cf4f.png',
|
||||
prompt: '将图1红色框内的【苹果】替换为【火龙果】',
|
||||
model: 'banana'
|
||||
},
|
||||
{
|
||||
"id": "830136945106498711",
|
||||
"fileUrl": "http://test.xueai.art/file/2026/4/2/69ce3b463888d39e4ed1cf4e.png",
|
||||
"prompt": "一个女孩在树下吃苹果",
|
||||
"model": "flux"
|
||||
id: '830136945106498711',
|
||||
fileUrl: 'http://test.xueai.art/file/2026/4/2/69ce3b463888d39e4ed1cf4e.png',
|
||||
prompt: '一个女孩在树下吃苹果',
|
||||
model: 'flux'
|
||||
},
|
||||
{
|
||||
"id": "830083758811001839",
|
||||
"fileUrl": "http://test.xueai.art/file/2026/4/2/69ce09be3888d39e4ed1cf48.png",
|
||||
"prompt": "一个女孩在树下吃苹果",
|
||||
"model": "flux"
|
||||
id: '830083758811001839',
|
||||
fileUrl: 'http://test.xueai.art/file/2026/4/2/69ce09be3888d39e4ed1cf48.png',
|
||||
prompt: '一个女孩在树下吃苹果',
|
||||
model: 'flux'
|
||||
},
|
||||
{
|
||||
"id": "829393290267734386",
|
||||
"fileUrl": "http://test.xueai.art/file/2026/3/31/69cb86b13888d39e4ed1cf32.png",
|
||||
"prompt": "将图1红色框内的【女孩】替换为【图2中的女孩】",
|
||||
"model": "banana"
|
||||
id: '829393290267734386',
|
||||
fileUrl: 'http://test.xueai.art/file/2026/3/31/69cb86b13888d39e4ed1cf32.png',
|
||||
prompt: '将图1红色框内的【女孩】替换为【图2中的女孩】',
|
||||
model: 'banana'
|
||||
},
|
||||
{
|
||||
"id": "829389466203337022",
|
||||
"fileUrl": "http://test.xueai.art/file/2026/3/31/69cb83223888d39e4ed1cf31.png",
|
||||
"prompt": "将图1红色框内的【<span style=\"font-size: 14px;\">女孩</span>】替换为【图2中的女孩】",
|
||||
"model": "banana"
|
||||
id: '829389466203337022',
|
||||
fileUrl: 'http://test.xueai.art/file/2026/3/31/69cb83223888d39e4ed1cf31.png',
|
||||
prompt: '将图1红色框内的【<span style="font-size: 14px;">女孩</span>】替换为【图2中的女孩】',
|
||||
model: 'banana'
|
||||
},
|
||||
{
|
||||
"id": "829388114303660338",
|
||||
"fileUrl": "http://test.xueai.art/file/2026/3/31/69cb81df3888d39e4ed1cf30.png",
|
||||
"prompt": "将图1红色框内的【女孩】替换为【男孩】",
|
||||
"model": "banana"
|
||||
id: '829388114303660338',
|
||||
fileUrl: 'http://test.xueai.art/file/2026/3/31/69cb81df3888d39e4ed1cf30.png',
|
||||
prompt: '将图1红色框内的【女孩】替换为【男孩】',
|
||||
model: 'banana'
|
||||
},
|
||||
{
|
||||
"id": "829381253919682782",
|
||||
"fileUrl": "http://test.xueai.art/file/2026/3/31/69cb7b7c3888d39e4ed1cf2d.png",
|
||||
"prompt": "将图1红色框内的【女孩】替换为【图2中的男孩】",
|
||||
"model": "banana"
|
||||
id: '829381253919682782',
|
||||
fileUrl: 'http://test.xueai.art/file/2026/3/31/69cb7b7c3888d39e4ed1cf2d.png',
|
||||
prompt: '将图1红色框内的【女孩】替换为【图2中的男孩】',
|
||||
model: 'banana'
|
||||
},
|
||||
{
|
||||
"id": "829324561060212843",
|
||||
"fileUrl": "http://test.xueai.art/file/2026/3/31/69cb46af3888d39e4ed1cf29.png",
|
||||
"prompt": "一个女孩在树下吃苹果",
|
||||
"model": "flux"
|
||||
id: '829324561060212843',
|
||||
fileUrl: 'http://test.xueai.art/file/2026/3/31/69cb46af3888d39e4ed1cf29.png',
|
||||
prompt: '一个女孩在树下吃苹果',
|
||||
model: 'flux'
|
||||
},
|
||||
{
|
||||
"id": "829319226454978647",
|
||||
"fileUrl": "http://test.xueai.art/file/2026/3/31/69cb41b73888d39e4ed1cf25.png,http://test.xueai.art/file/2026/3/31/69cb41b73888d39e4ed1cf26.png,http://test.xueai.art/file/2026/3/31/69cb41b73888d39e4ed1cf27.png,http://test.xueai.art/file/2026/3/31/69cb41b73888d39e4ed1cf28.png",
|
||||
"prompt": "这是一幅治愈写实风格的插画,画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶,空调外机挂在墙边,金属防护栏环绕四周,外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅,旁边还有小凳。彩色水桶,一个是红色,一个是蓝色,放置在角落,花盆里栽种着金桔,水仙花、蝴蝶兰,整齐排列,大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树,绿意浓郁,与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼,整个场景被绿意环绕,明亮柔和的光线洒下,充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格",
|
||||
"model": "flux"
|
||||
id: '829319226454978647',
|
||||
fileUrl: 'http://test.xueai.art/file/2026/3/31/69cb41b73888d39e4ed1cf25.png,http://test.xueai.art/file/2026/3/31/69cb41b73888d39e4ed1cf26.png,http://test.xueai.art/file/2026/3/31/69cb41b73888d39e4ed1cf27.png,http://test.xueai.art/file/2026/3/31/69cb41b73888d39e4ed1cf28.png',
|
||||
prompt: '这是一幅治愈写实风格的插画,画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶,空调外机挂在墙边,金属防护栏环绕四周,外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅,旁边还有小凳。彩色水桶,一个是红色,一个是蓝色,放置在角落,花盆里栽种着金桔,水仙花、蝴蝶兰,整齐排列,大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树,绿意浓郁,与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼,整个场景被绿意环绕,明亮柔和的光线洒下,充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格',
|
||||
model: 'flux'
|
||||
},
|
||||
{
|
||||
"id": "829317957644464188",
|
||||
"fileUrl": "http://test.xueai.art/file/2026/3/31/69cb40893888d39e4ed1cf20.png",
|
||||
"prompt": "这是一幅治愈写实风格的插画,画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶,空调外机挂在墙边,金属防护栏环绕四周,外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅,旁边还有小凳。彩色水桶,一个是红色,一个是蓝色,放置在角落,花盆里栽种着金桔,水仙花、蝴蝶兰,整齐排列,大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树,绿意浓郁,与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼,整个场景被绿意环绕,明亮柔和的光线洒下,充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格",
|
||||
"model": "flux"
|
||||
id: '829317957644464188',
|
||||
fileUrl: 'http://test.xueai.art/file/2026/3/31/69cb40893888d39e4ed1cf20.png',
|
||||
prompt: '这是一幅治愈写实风格的插画,画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶,空调外机挂在墙边,金属防护栏环绕四周,外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅,旁边还有小凳。彩色水桶,一个是红色,一个是蓝色,放置在角落,花盆里栽种着金桔,水仙花、蝴蝶兰,整齐排列,大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树,绿意浓郁,与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼,整个场景被绿意环绕,明亮柔和的光线洒下,充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格',
|
||||
model: 'flux'
|
||||
},
|
||||
{
|
||||
"id": "829305227994738709",
|
||||
"fileUrl": "http://test.xueai.art/file/2026/3/31/69cb34ae3888d39e4ed1cf1e.png",
|
||||
"prompt": "这是一幅治愈写实风格的插画,画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶,空调外机挂在墙边,金属防护栏环绕四周,外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅,旁边还有小凳。彩色水桶,一个是红色,一个是蓝色,放置在角落,花盆里栽种着金桔,水仙花、蝴蝶兰,整齐排列,大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树,绿意浓郁,与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼,整个场景被绿意环绕,明亮柔和的光线洒下,充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格",
|
||||
"model": "flux"
|
||||
id: '829305227994738709',
|
||||
fileUrl: 'http://test.xueai.art/file/2026/3/31/69cb34ae3888d39e4ed1cf1e.png',
|
||||
prompt: '这是一幅治愈写实风格的插画,画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶,空调外机挂在墙边,金属防护栏环绕四周,外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅,旁边还有小凳。彩色水桶,一个是红色,一个是蓝色,放置在角落,花盆里栽种着金桔,水仙花、蝴蝶兰,整齐排列,大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树,绿意浓郁,与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼,整个场景被绿意环绕,明亮柔和的光线洒下,充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格',
|
||||
model: 'flux'
|
||||
},
|
||||
{
|
||||
"id": "829068575099597628",
|
||||
"fileUrl": "http://test.xueai.art/file/2026/3/30/69ca58473888d39e0bb8728b.png",
|
||||
"prompt": "",
|
||||
"model": ""
|
||||
id: '829068575099597628',
|
||||
fileUrl: 'http://test.xueai.art/file/2026/3/30/69ca58473888d39e0bb8728b.png',
|
||||
prompt: '',
|
||||
model: ''
|
||||
}
|
||||
];
|
||||
]
|
||||
|
||||
@ -7,8 +7,8 @@
|
||||
<div class="input-group">
|
||||
<label>W</label>
|
||||
<input
|
||||
type="number"
|
||||
v-model.number="localWidth"
|
||||
type="number"
|
||||
:min="minW"
|
||||
:max="maxW"
|
||||
@input="onWidthChange"
|
||||
@ -21,8 +21,8 @@
|
||||
<div class="input-group">
|
||||
<label>H</label>
|
||||
<input
|
||||
type="number"
|
||||
v-model.number="localHeight"
|
||||
type="number"
|
||||
:min="minH"
|
||||
:max="maxH"
|
||||
@input="onHeightChange"
|
||||
@ -41,9 +41,9 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Popover from '@/components/Popover/index.vue'
|
||||
import lockIcon from '@/assets/dialog/lock.svg'
|
||||
import lockNoIcon from '@/assets/dialog/lockNo.svg'
|
||||
import Popover from '@/components/Popover/index.vue'
|
||||
|
||||
const props = defineProps({
|
||||
width: { type: Number, default: 1024 },
|
||||
@ -51,7 +51,7 @@ const props = defineProps({
|
||||
minW: { type: Number, default: 256 },
|
||||
maxW: { type: Number, default: 6197 },
|
||||
minH: { type: Number, default: 256 },
|
||||
maxH: { type: Number, default: 4096 },
|
||||
maxH: { type: Number, default: 4096 }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:width', 'update:height'])
|
||||
|
||||
@ -37,7 +37,7 @@
|
||||
<div class="size-inputs">
|
||||
<div class="input-group">
|
||||
<label>W</label>
|
||||
<input type="number" v-model.number="width" @input="updateWidth" :disabled="isLocked">
|
||||
<input v-model.number="width" type="number" :disabled="isLocked" @input="updateWidth">
|
||||
</div>
|
||||
<div class="lock-icon" :class="{ locked: isLocked }" @click="toggleLock">
|
||||
<img :src="isLocked ? lockIcon : lockNoIcon" alt="约束比例">
|
||||
@ -45,7 +45,7 @@
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label>H</label>
|
||||
<input type="number" v-model.number="height" @input="updateHeight" :disabled="isLocked">
|
||||
<input v-model.number="height" type="number" :disabled="isLocked" @input="updateHeight">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -60,9 +60,9 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Popover from '@/components/Popover/index.vue'
|
||||
import lockIcon from '@/assets/dialog/lock.svg'
|
||||
import lockNoIcon from '@/assets/dialog/lockNo.svg'
|
||||
import Popover from '@/components/Popover/index.vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
@ -95,7 +95,7 @@ const props = defineProps({
|
||||
},
|
||||
allowCustom: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
default: true
|
||||
},
|
||||
resolutionOptions: {
|
||||
type: Array,
|
||||
@ -461,5 +461,4 @@ watch(() => [props.modelValue, props.resolution], () => {
|
||||
background: #f5f6f7;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@ -19,8 +19,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<el-upload
|
||||
ref="uploadRef"
|
||||
v-show="false"
|
||||
ref="uploadRef"
|
||||
:action="uploadurl"
|
||||
multiple
|
||||
:limit="limit"
|
||||
|
||||
@ -15,10 +15,9 @@
|
||||
import Select from '@/components/Select/index.vue'
|
||||
import { fetchPlatformModels, getPlatformCode } from '@/utils/modelApi'
|
||||
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: String, default: 'Flux 2' },
|
||||
typeValue: { type: String, default: 'text' },
|
||||
typeValue: { type: String, default: 'text' }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'update:typeValue'])
|
||||
@ -28,7 +27,7 @@ const platformModels = ref([])
|
||||
const categoryMap = [
|
||||
{ tag: 'text', label: '生成模型', inputType: 'text' },
|
||||
{ tag: 'edit', label: '编辑模型', inputType: 'image' },
|
||||
{ tag: 'vision', label: '视觉理解模型', inputType: 'vision' },
|
||||
{ tag: 'vision', label: '视觉理解模型', inputType: 'vision' }
|
||||
]
|
||||
|
||||
function parseValue(encoded) {
|
||||
@ -44,14 +43,14 @@ function encodeValue(tag, modelName) {
|
||||
|
||||
function findTagForModel(modelName) {
|
||||
for (const cat of categoryMap) {
|
||||
const model = platformModels.value.find(m => (m.display_name || m.name) === modelName && m.tags?.includes(cat.tag))
|
||||
const model = platformModels.value.find((m) => (m.display_name || m.name) === modelName && m.tags?.includes(cat.tag))
|
||||
if (model) return cat.tag
|
||||
}
|
||||
return 'text'
|
||||
}
|
||||
|
||||
function tagToInputType(tag) {
|
||||
const cat = categoryMap.find(c => c.tag === tag)
|
||||
const cat = categoryMap.find((c) => c.tag === tag)
|
||||
return cat?.inputType || 'text'
|
||||
}
|
||||
|
||||
@ -67,7 +66,7 @@ const selectValue = computed({
|
||||
if (!parsed) return
|
||||
emit('update:modelValue', parsed.modelName)
|
||||
emit('update:typeValue', tagToInputType(parsed.tag))
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
// 从 API 加载模型列表
|
||||
@ -89,26 +88,26 @@ const modelGroups = computed(() => {
|
||||
if (models.length === 0) return []
|
||||
|
||||
return categoryMap
|
||||
.filter(cat => models.some(m => m.tags?.includes(cat.tag)))
|
||||
.map(cat => ({
|
||||
.filter((cat) => models.some((m) => m.tags?.includes(cat.tag)))
|
||||
.map((cat) => ({
|
||||
label: cat.label,
|
||||
options: models
|
||||
.filter(m => m.tags?.includes(cat.tag))
|
||||
.map(m => ({
|
||||
.filter((m) => m.tags?.includes(cat.tag))
|
||||
.map((m) => ({
|
||||
value: `${cat.tag}::${m.display_name || m.name}`,
|
||||
label: m.display_name || m.name,
|
||||
disabled: m.disabled || false,
|
||||
disabled: m.disabled || false
|
||||
}))
|
||||
.sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: 'base' })),
|
||||
.sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: 'base' }))
|
||||
}))
|
||||
})
|
||||
|
||||
// 模型列表加载后自动纠正不可用模型
|
||||
watch(platformModels, (models) => {
|
||||
if (models.length === 0) return
|
||||
const currentModel = models.find(m => (m.display_name || m.name) === props.modelValue || m.id === props.modelValue)
|
||||
const currentModel = models.find((m) => (m.display_name || m.name) === props.modelValue || m.id === props.modelValue)
|
||||
if (!currentModel || currentModel.disabled) {
|
||||
const firstEnabled = models.find(m => !m.disabled)
|
||||
const firstEnabled = models.find((m) => !m.disabled)
|
||||
if (firstEnabled) {
|
||||
emit('update:modelValue', firstEnabled.display_name || firstEnabled.name)
|
||||
emit('update:typeValue', tagToInputType(findTagForModel(firstEnabled.display_name || firstEnabled.name)))
|
||||
@ -121,9 +120,9 @@ watch(() => props.modelValue, (newValue) => {
|
||||
if (!newValue) return
|
||||
const models = platformModels.value
|
||||
if (models.length === 0) return
|
||||
const currentModel = models.find(m => (m.display_name || m.name) === newValue)
|
||||
const currentModel = models.find((m) => (m.display_name || m.name) === newValue)
|
||||
if (currentModel && currentModel.disabled) {
|
||||
const firstEnabled = models.find(m => !m.disabled)
|
||||
const firstEnabled = models.find((m) => !m.disabled)
|
||||
if (firstEnabled) {
|
||||
emit('update:modelValue', firstEnabled.display_name || firstEnabled.name)
|
||||
emit('update:typeValue', tagToInputType(findTagForModel(firstEnabled.display_name || firstEnabled.name)))
|
||||
|
||||
@ -18,12 +18,12 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Select from '@/components/Select/index.vue'
|
||||
|
||||
import videoPattern2 from '@/assets/dialog/videoPattern2.svg'
|
||||
|
||||
import videoPattern4 from '@/assets/dialog/videoPattern4.svg'
|
||||
import videoPattern5 from '@/assets/dialog/videoPattern5.svg'
|
||||
import videoPattern6 from '@/assets/dialog/videoPattern6.svg'
|
||||
import Select from '@/components/Select/index.vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
@ -44,12 +44,12 @@ const quantityOptions = [
|
||||
{ value: '首尾帧', label: '首尾帧', labelText: '首尾帧', icon: videoPattern2 },
|
||||
{ value: '数字人', label: '数字人', labelText: '数字人', icon: videoPattern2 },
|
||||
{ value: '全能参考', label: '全能参考', labelText: '全能参考', icon: videoPattern4 },
|
||||
{ value: '智能多帧', label: '智能多帧', labelText: '智能多帧', icon: videoPattern5 },
|
||||
{ value: '智能多帧', label: '智能多帧', labelText: '智能多帧', icon: videoPattern5 },
|
||||
{ value: '主体参考', label: '主体参考', labelText: '主体参考', icon: videoPattern6 }
|
||||
]
|
||||
|
||||
const selectedIcon = computed(() => {
|
||||
const option = quantityOptions.find(opt => opt.value === quantity.value)
|
||||
const option = quantityOptions.find((opt) => opt.value === quantity.value)
|
||||
return option ? option.icon : videoPattern1
|
||||
})
|
||||
</script>
|
||||
@ -62,20 +62,20 @@ const selectedIcon = computed(() => {
|
||||
border-radius: 10px;
|
||||
border: 1px solid #E8E9EB;
|
||||
background: #f5f6f7;
|
||||
|
||||
|
||||
&:hover {
|
||||
background: #e9eaeb;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
:deep(.select-text) {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
|
||||
:deep(.dropdown-menu) {
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
|
||||
:deep(.dropdown-item) {
|
||||
min-width: 80px;
|
||||
justify-content: start;
|
||||
|
||||
@ -4,8 +4,8 @@
|
||||
<div class="section">
|
||||
<h3>选择比例</h3>
|
||||
<div class="proportion-options" :style="{ marginBottom: props.type === 'Video' ? '0px' : '20px' }">
|
||||
<div
|
||||
v-for="item in proportionOptions"
|
||||
<div
|
||||
v-for="item in proportionOptions"
|
||||
:key="item.value"
|
||||
class="proportion-item"
|
||||
:class="{ active: proportion === item.value }"
|
||||
@ -20,8 +20,8 @@
|
||||
<div class="section">
|
||||
<h3>选择分辨率</h3>
|
||||
<div class="resolution-options">
|
||||
<div
|
||||
v-for="item in resolutionOptions"
|
||||
<div
|
||||
v-for="item in resolutionOptions"
|
||||
:key="item.value"
|
||||
class="resolution-item"
|
||||
:class="{ active: resolution === item.value }"
|
||||
@ -107,7 +107,7 @@ const getProportionStyle = (value) => {
|
||||
const [w, h] = value.split(':').map(Number)
|
||||
const aspectRatio = w / h
|
||||
const baseSize = 20
|
||||
|
||||
|
||||
if (aspectRatio > 1) {
|
||||
return {
|
||||
'--width': `${baseSize}px`,
|
||||
@ -148,11 +148,11 @@ const getProportionStyle = (value) => {
|
||||
.section{
|
||||
margin-bottom: 20px;
|
||||
border-radius: 20px;
|
||||
|
||||
|
||||
&:last-child{
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
|
||||
h3{
|
||||
font-family: "Microsoft YaHei";
|
||||
font-size: 12px;
|
||||
@ -185,7 +185,7 @@ const getProportionStyle = (value) => {
|
||||
border-radius: 5px;
|
||||
text-align: bottom;
|
||||
color: #999;
|
||||
|
||||
|
||||
&::before{
|
||||
content: '';
|
||||
width: var(--width, 20px);
|
||||
@ -195,11 +195,11 @@ const getProportionStyle = (value) => {
|
||||
transition: all 0.2s ease;
|
||||
border: 2px solid #999;
|
||||
}
|
||||
|
||||
|
||||
&:hover{
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
|
||||
&.active{
|
||||
color: #000F33;
|
||||
background: #ffffff;
|
||||
@ -228,11 +228,11 @@ const getProportionStyle = (value) => {
|
||||
text-align: center;
|
||||
transition: all 0.2s ease;
|
||||
color: #666;
|
||||
|
||||
|
||||
&:hover{
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
|
||||
&.active{
|
||||
background: #ffffff;
|
||||
color: #000000;
|
||||
|
||||
@ -53,20 +53,20 @@ const quantityOptions = computed(() => props.options)
|
||||
border-radius: 10px;
|
||||
border: 1px solid #E8E9EB;
|
||||
background: #f5f6f7;
|
||||
|
||||
|
||||
&:hover {
|
||||
background: #e9eaeb;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
:deep(.select-text) {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
|
||||
:deep(.dropdown-menu) {
|
||||
min-width: 136px;
|
||||
}
|
||||
|
||||
|
||||
:deep(.dropdown-item) {
|
||||
min-width: 80px;
|
||||
justify-content: center;
|
||||
|
||||
@ -25,9 +25,9 @@
|
||||
</div>
|
||||
<el-upload
|
||||
v-for="i in maxImages"
|
||||
v-show="false"
|
||||
:key="i"
|
||||
:ref="el => setUploadRef(el, i - 1)"
|
||||
v-show="false"
|
||||
:action="uploadurl"
|
||||
:limit="1"
|
||||
:before-upload="beforeUpload"
|
||||
@ -90,9 +90,9 @@ watch(() => props.modelValue, async (newVal) => {
|
||||
if (isUploading.value) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
imageList.value = [...newVal]
|
||||
|
||||
|
||||
const newPreviewList = []
|
||||
for (const img of newVal) {
|
||||
let previewImg = { ...img }
|
||||
@ -140,16 +140,16 @@ const beforeUpload = (rawFile) => {
|
||||
|
||||
const handleSuccess = (response, uploadFile, index) => {
|
||||
ElMessage.success('上传成功')
|
||||
|
||||
|
||||
isUploading.value = true
|
||||
|
||||
|
||||
const localUrl = URL.createObjectURL(uploadFile.raw)
|
||||
|
||||
|
||||
const newImage = {
|
||||
uid: uploadFile.uid,
|
||||
url: response.url
|
||||
}
|
||||
|
||||
|
||||
if (imageList.value[index]) {
|
||||
const previewItem = localPreviewList.value[index]
|
||||
if (previewItem && previewItem.url && previewItem.url.startsWith('blob:')) {
|
||||
@ -169,9 +169,9 @@ const handleSuccess = (response, uploadFile, index) => {
|
||||
serverUrl: response.url
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
emit('update:modelValue', [...imageList.value])
|
||||
|
||||
|
||||
nextTick(() => {
|
||||
isUploading.value = false
|
||||
})
|
||||
@ -186,7 +186,7 @@ const handleDelete = (index) => {
|
||||
if (previewItem && previewItem.url && previewItem.url.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(previewItem.url)
|
||||
}
|
||||
|
||||
|
||||
localPreviewList.value.splice(index, 1)
|
||||
imageList.value.splice(index, 1)
|
||||
emit('update:modelValue', [...imageList.value])
|
||||
|
||||
@ -44,15 +44,14 @@ const fetchConfig = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fetchConfig()
|
||||
|
||||
watch(() => videoConfig.value, (newConfig) => {
|
||||
const models = newConfig[props.videoPattern] || []
|
||||
if (models.length > 0) {
|
||||
const enabledModels = models.filter(m => !m.disabled)
|
||||
const enabledModels = models.filter((m) => !m.disabled)
|
||||
if (enabledModels.length > 0) {
|
||||
const currentModelExists = enabledModels.find(m => m.value === props.modelValue)
|
||||
const currentModelExists = enabledModels.find((m) => m.value === props.modelValue)
|
||||
if (!currentModelExists) {
|
||||
model.value = enabledModels[0].value
|
||||
}
|
||||
@ -60,7 +59,6 @@ watch(() => videoConfig.value, (newConfig) => {
|
||||
}
|
||||
}, { deep: true })
|
||||
|
||||
|
||||
const model = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => {
|
||||
@ -91,9 +89,9 @@ const getModelType = (value) => {
|
||||
watch(() => props.videoPattern, (newPattern) => {
|
||||
const models = videoConfig.value[newPattern] || []
|
||||
if (models.length > 0) {
|
||||
const enabledModels = models.filter(m => !m.disabled)
|
||||
const enabledModels = models.filter((m) => !m.disabled)
|
||||
if (enabledModels.length > 0) {
|
||||
const currentModelExists = enabledModels.find(m => m.value === props.modelValue)
|
||||
const currentModelExists = enabledModels.find((m) => m.value === props.modelValue)
|
||||
if (!currentModelExists) {
|
||||
model.value = enabledModels[0].value
|
||||
}
|
||||
@ -103,9 +101,9 @@ watch(() => props.videoPattern, (newPattern) => {
|
||||
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
const models = videoConfig.value[props.videoPattern] || []
|
||||
const currentModel = models.find(m => m.value === newValue)
|
||||
const currentModel = models.find((m) => m.value === newValue)
|
||||
if (currentModel && currentModel.disabled) {
|
||||
const enabledModels = models.filter(m => !m.disabled)
|
||||
const enabledModels = models.filter((m) => !m.disabled)
|
||||
if (enabledModels.length > 0) {
|
||||
model.value = enabledModels[0].value
|
||||
}
|
||||
@ -121,24 +119,24 @@ watch(() => props.modelValue, (newValue) => {
|
||||
border-radius: 10px;
|
||||
border: 1px solid #E8E9EB;
|
||||
background: #f5f6f7;
|
||||
|
||||
|
||||
&:hover {
|
||||
background: #e9eaeb;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
:deep(.select-text) {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
|
||||
:deep(.dropdown-menu) {
|
||||
max-height: 510px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
|
||||
:deep(.dropdown-item) {
|
||||
min-width: 120px;
|
||||
|
||||
|
||||
&.active {
|
||||
background: rgba(0, 15, 51, 0.10);
|
||||
color: #000F33;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { useDisplayStore, useUserStore } from '@/stores'
|
||||
import { useUserStore } from '@/stores'
|
||||
import { getToken, setToken } from '@/utils/auth'
|
||||
|
||||
const routes = [
|
||||
@ -30,7 +30,7 @@ const router = createRouter({
|
||||
})
|
||||
|
||||
router.beforeEach(async (to, from) => {
|
||||
if(to.query.token){
|
||||
if (to.query.token) {
|
||||
setToken(to.query.token)
|
||||
} else {
|
||||
// 检查是否有 token
|
||||
|
||||
@ -8,12 +8,12 @@ const DisplayStoreSetup = () => {
|
||||
const isLoading = ref(false)
|
||||
const currentResultData = ref(null)
|
||||
const dialogBoxRef = ref(null)
|
||||
|
||||
|
||||
const canvasVisible = ref(false)
|
||||
const canvasImage = ref('')
|
||||
const canvasReferenceImages = ref([])
|
||||
const canvasSource = ref('')
|
||||
|
||||
|
||||
const addGeneratingItem = (item) => {
|
||||
const newItem = {
|
||||
id: item.taskId || crypto.randomUUID(),
|
||||
@ -26,29 +26,29 @@ const DisplayStoreSetup = () => {
|
||||
tempList.value.unshift(newItem)
|
||||
return newItem
|
||||
}
|
||||
|
||||
|
||||
const updateItemToSuccess = (taskId, fileUrls) => {
|
||||
const index = tempList.value.findIndex(item => item.id === taskId)
|
||||
const index = tempList.value.findIndex((item) => item.id === taskId)
|
||||
if (index !== -1) {
|
||||
tempList.value[index].status = 'success'
|
||||
tempList.value[index].files = Array.isArray(fileUrls) ? fileUrls : [fileUrls]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const initHistoryList = (historyList) => {
|
||||
tempList.value = historyList
|
||||
currentPage.value = 1
|
||||
hasMoreData.value = true
|
||||
}
|
||||
|
||||
|
||||
const prependHistoryList = (historyList) => {
|
||||
tempList.value = [...historyList, ...tempList.value]
|
||||
}
|
||||
|
||||
|
||||
const appendHistoryList = (historyList) => {
|
||||
tempList.value = [...tempList.value, ...historyList]
|
||||
}
|
||||
|
||||
|
||||
const resetPagination = () => {
|
||||
currentPage.value = 0
|
||||
hasMoreData.value = true
|
||||
@ -56,26 +56,26 @@ const DisplayStoreSetup = () => {
|
||||
}
|
||||
|
||||
const deleteHistoryItem = (id) => {
|
||||
const index = tempList.value.findIndex(item => item.id === id)
|
||||
const index = tempList.value.findIndex((item) => item.id === id)
|
||||
if (index !== -1) {
|
||||
tempList.value.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const scrollToBottom = async () => {
|
||||
const refValue = scrollerRef.value
|
||||
|
||||
|
||||
if (!refValue) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
if (typeof refValue.scrollToBottom === 'function') {
|
||||
await nextTick()
|
||||
refValue.scrollToBottom()
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
const scrollerEl = refValue.$el
|
||||
if (scrollerEl) {
|
||||
const viewport = scrollerEl.querySelector('.vue-recycle-scroller__viewport')
|
||||
@ -83,7 +83,7 @@ const DisplayStoreSetup = () => {
|
||||
viewport.scrollTop = viewport.scrollHeight
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (typeof refValue.scrollToItem === 'function' && tempList.value && tempList.value.length > 0) {
|
||||
await nextTick()
|
||||
refValue.scrollToItem(tempList.value.length - 1)
|
||||
@ -92,28 +92,28 @@ const DisplayStoreSetup = () => {
|
||||
console.error('滚动出错:', error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const setResultData = (data) => {
|
||||
currentResultData.value = data
|
||||
}
|
||||
|
||||
|
||||
const setDialogBoxRef = (ref) => {
|
||||
dialogBoxRef.value = ref
|
||||
}
|
||||
|
||||
|
||||
const triggerGenerateWithResult = async () => {
|
||||
if (dialogBoxRef.value && currentResultData.value) {
|
||||
await dialogBoxRef.value.fillParamsFromResult(currentResultData.value)
|
||||
await dialogBoxRef.value.handleStart()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const fillParamsForEdit = () => {
|
||||
if (dialogBoxRef.value && currentResultData.value) {
|
||||
dialogBoxRef.value.fillParamsFromResult(currentResultData.value)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const openCanvas = (data) => {
|
||||
if (typeof data === 'string') {
|
||||
canvasImage.value = data
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
const ParamStoreSetup = () => {
|
||||
|
||||
return {
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ export async function generateFilename(url, prefix = 'image') {
|
||||
|
||||
// 如果URL中没有文件名或扩展名,根据类型生成
|
||||
if (!filename || !filename.includes('.')) {
|
||||
const timestamp = new Date().getTime()
|
||||
const timestamp = Date.now()
|
||||
// 根据URL内容推断文件类型,否则默认为png
|
||||
const extension = url.includes('.jpg') || url.includes('.jpeg')
|
||||
? '.jpg'
|
||||
@ -21,7 +21,7 @@ export async function generateFilename(url, prefix = 'image') {
|
||||
} catch (error) {
|
||||
console.error('URL解析失败:', error)
|
||||
// 如果URL解析失败,生成默认文件名
|
||||
const timestamp = new Date().getTime()
|
||||
const timestamp = Date.now()
|
||||
return `${prefix}_${timestamp}.png`
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,21 +1,20 @@
|
||||
<template>
|
||||
<div style="width: 100%;display: flex;justify-content: center;align-items: center;transform: rotate(180deg);">
|
||||
<div class="primary-box" :class="{ 'none-primary-box': props.item.status === 'none' }">
|
||||
|
||||
<div class="prompt-container" ref="promptContainerRef">
|
||||
<div class="prompt-wrapper" ref="promptWrapperRef">
|
||||
<div class="prompt" ref="promptRef" :class="{ 'expanded': isHovering }" @mouseenter="isHovering = true" @mouseleave="isHovering = false">
|
||||
<div ref="promptContainerRef" class="prompt-container">
|
||||
<div ref="promptWrapperRef" class="prompt-wrapper">
|
||||
<div ref="promptRef" class="prompt" :class="{ expanded: isHovering }" @mouseenter="isHovering = true" @mouseleave="isHovering = false">
|
||||
<span class="prompt-text">
|
||||
{{ props.item.generateData.prompt || '生成图片' }}
|
||||
<i-ep-DocumentCopy class="Copy" @click.stop="copyPrompt"/>
|
||||
<i-ep-DocumentCopy class="Copy" @click.stop="copyPrompt" />
|
||||
</span>
|
||||
<div class="generate-data internal" v-show="!isHovering && !showExternalGenerateData">
|
||||
<div v-show="!isHovering && !showExternalGenerateData" class="generate-data internal">
|
||||
<div class="detailed-data first-detailed-data">{{ props.item.generateData.model }}</div>
|
||||
<div class="detailed-data">{{ props.item.generateData.proportion }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="generate-data external" v-show="!isHovering && showExternalGenerateData">
|
||||
<div v-show="!isHovering && showExternalGenerateData" class="generate-data external">
|
||||
<div class="detailed-data first-detailed-data">{{ props.item.generateData.model }}</div>
|
||||
<div class="detailed-data">{{ props.item.generateData.proportion }}</div>
|
||||
</div>
|
||||
@ -49,7 +48,7 @@
|
||||
|
||||
<!-- 已完成 图片 -->
|
||||
<div v-if="props.item.status === 'success' && props.item.type === 'Painting'" class="box success-box">
|
||||
<div v-for="(file, index) in props.item.files" :key="index" class="one-box" :class="{ 'collected': isCollected(file) }" @mouseenter="hoverIndex = index" @mouseleave="hoverIndex = -1">
|
||||
<div v-for="(file, index) in props.item.files" :key="index" class="one-box" :class="{ collected: isCollected(file) }" @mouseenter="hoverIndex = index" @mouseleave="hoverIndex = -1">
|
||||
<!-- <img :src="file" alt="index" class="img" /> -->
|
||||
<Img :src="file" alt="index" class="img" />
|
||||
|
||||
@ -58,14 +57,14 @@
|
||||
<span v-if="hoverIndex === index" class="line" />
|
||||
<div class="left-top-btn collect-btn" @click="addCollection(file)"><img :src="isCollected(file) ? collectionActiveIcon : collectionIcon" /></div>
|
||||
</div>
|
||||
|
||||
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
content="画笔"
|
||||
placement="top"
|
||||
:hide-after="0"
|
||||
>
|
||||
<div @click.stop="AIbrush(file, index)" class="bottom-brush">
|
||||
<div class="bottom-brush" @click.stop="AIbrush(file, index)">
|
||||
<img :src="brush" />
|
||||
</div>
|
||||
</el-tooltip>
|
||||
@ -74,7 +73,7 @@
|
||||
|
||||
<!-- 已完成 视频 -->
|
||||
<div v-if="props.item.status === 'success' && props.item.type === 'Video'" class="box success-box">
|
||||
<div class="one-box" :class="{ 'collected': isCollected(props.item.files[0]) }" @mouseenter="hoverIndex = 0" @mouseleave="hoverIndex = -1">
|
||||
<div class="one-box" :class="{ collected: isCollected(props.item.files[0]) }" @mouseenter="hoverIndex = 0" @mouseleave="hoverIndex = -1">
|
||||
<!-- <img :src="file" alt="index" class="img" /> -->
|
||||
<video :src="props.item.files[0]" class="video" controls playsinline />
|
||||
|
||||
@ -97,16 +96,16 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { cancelOrCollect, deleteGenerateHistory } from '@/apis/display'
|
||||
import againGenerateIcon from '@/assets/display/againGenerate.svg'
|
||||
import brush from '@/assets/display/brush.svg'
|
||||
import collectionIcon from '@/assets/display/collection.svg'
|
||||
import collectionActiveIcon from '@/assets/display/collection-active.svg'
|
||||
import collectionIcon from '@/assets/display/collection.svg'
|
||||
import deleteImageIcon from '@/assets/display/deleteImage.svg'
|
||||
import reEditIcon from '@/assets/display/reEdit.svg'
|
||||
import Img from '@/components/Img/index.vue'
|
||||
import { useDisplayStore, useParamStore, useUserStore } from '@/stores'
|
||||
import { downloadImage } from '@/utils/downloadImage.js'
|
||||
import reEditIcon from '@/assets/display/reEdit.svg'
|
||||
import againGenerateIcon from '@/assets/display/againGenerate.svg'
|
||||
import deleteImageIcon from '@/assets/display/deleteImage.svg'
|
||||
import Img from '@/components/Img/index.vue'
|
||||
import { cancelOrCollect, deleteGenerateHistory } from '@/apis/display'
|
||||
|
||||
const props = defineProps({
|
||||
item: {
|
||||
@ -134,22 +133,22 @@ const checkTextOverflow = () => {
|
||||
const lineHeight = 22.5
|
||||
const padding = 8
|
||||
const twoLineHeight = lineHeight * 2 + padding
|
||||
|
||||
|
||||
promptRef.value.style.maxHeight = 'none'
|
||||
promptRef.value.style.overflow = 'hidden'
|
||||
|
||||
|
||||
const actualHeight = promptRef.value.scrollHeight
|
||||
const lineCount = Math.ceil((actualHeight - padding) / lineHeight)
|
||||
|
||||
|
||||
if (!isHovering.value) {
|
||||
promptRef.value.style.maxHeight = `${twoLineHeight}px`
|
||||
promptRef.value.style.overflow = 'hidden'
|
||||
}
|
||||
|
||||
|
||||
showExternalGenerateData.value = lineCount >= 3
|
||||
|
||||
|
||||
if (!isHovering.value) {
|
||||
if(lineCount >= 3){
|
||||
if (lineCount >= 3) {
|
||||
promptContainerRef.value.style.height = `${twoLineHeight}px`
|
||||
} else {
|
||||
promptContainerRef.value.style.height = `${actualHeight}px`
|
||||
@ -163,8 +162,8 @@ watch(isHovering, (newVal) => {
|
||||
if (promptRef.value) {
|
||||
const lineHeight = 22.5
|
||||
const padding = 8
|
||||
const twoLineHeight = lineHeight * 2 + padding
|
||||
|
||||
const twoLineHeight = lineHeight * 2 + padding
|
||||
|
||||
if (newVal) {
|
||||
promptRef.value.style.maxHeight = 'none'
|
||||
promptRef.value.style.overflow = 'visible'
|
||||
@ -208,7 +207,7 @@ const AIbrush = (file, index) => {
|
||||
}
|
||||
|
||||
const reEdit = () => {
|
||||
if(props.item.generateData?.modelType === 'edit'){
|
||||
if (props.item.generateData?.modelType === 'edit') {
|
||||
ElMessage.error('画笔生成的任务不能重新编辑')
|
||||
return
|
||||
}
|
||||
@ -217,7 +216,7 @@ const reEdit = () => {
|
||||
}
|
||||
|
||||
const againGenerate = () => {
|
||||
if(props.item.generateData?.modelType === 'edit'){
|
||||
if (props.item.generateData?.modelType === 'edit') {
|
||||
ElMessage.error('画笔生成的任务不能再次生成')
|
||||
return
|
||||
}
|
||||
@ -276,7 +275,7 @@ const addCollection = async (url) => {
|
||||
const res = await cancelOrCollect({
|
||||
taskId: props.item.id,
|
||||
userId: useUser.userInfo.id,
|
||||
url: url,
|
||||
url
|
||||
})
|
||||
if (res.success) {
|
||||
ElMessage.success(res.message || '操作成功')
|
||||
@ -342,7 +341,7 @@ const copyPrompt = async () => {
|
||||
width: 102%;
|
||||
z-index: 5;
|
||||
width: auto;
|
||||
|
||||
|
||||
&.expanded{
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 4px;
|
||||
@ -381,12 +380,12 @@ const copyPrompt = async () => {
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
|
||||
|
||||
&.internal{
|
||||
display: inline-flex;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
|
||||
&.external{
|
||||
position: absolute;
|
||||
right: -16px;
|
||||
|
||||
@ -34,7 +34,7 @@
|
||||
</div>
|
||||
<span class="line"></span>
|
||||
<div class="btn">
|
||||
<Select v-model="selectedFavorite" :options="favoriteOptions" width="auto" >
|
||||
<Select v-model="selectedFavorite" :options="favoriteOptions" width="auto">
|
||||
<template #prefix>
|
||||
<i-ep-Star />
|
||||
</template>
|
||||
@ -43,8 +43,8 @@
|
||||
</div>
|
||||
|
||||
<VirtualScroller
|
||||
ref="scrollerRef"
|
||||
v-if="props.if"
|
||||
ref="scrollerRef"
|
||||
:items="list"
|
||||
key-field="id"
|
||||
:estimated-height="300"
|
||||
@ -66,17 +66,17 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useDisplayStore, useParamStore, useUserStore } from '@/stores'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import Set from './components/set.vue'
|
||||
import RefreshOverlay from './components/RefreshOverlay.vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { requestTaskHistory } from '@/apis/display'
|
||||
import Canvas from '@/components/canvas/index.vue'
|
||||
import Select from '@/components/Select/index.vue'
|
||||
import { VirtualScroller } from '@/components/virtual-scroller'
|
||||
import Canvas from '@/components/canvas/index.vue'
|
||||
import { requestTaskHistory } from '@/apis/display'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { getChargeType } from '@/utils/taskPolling'
|
||||
import { useDisplayStore, useParamStore, useUserStore } from '@/stores'
|
||||
import { getPlatformCode } from '@/utils/modelApi'
|
||||
import { getChargeType } from '@/utils/taskPolling'
|
||||
import RefreshOverlay from './components/RefreshOverlay.vue'
|
||||
import Set from './components/set.vue'
|
||||
|
||||
const props = defineProps({
|
||||
if: {
|
||||
@ -138,7 +138,7 @@ const toggleDisplay = (newValue, oldValue) => {
|
||||
const conversion = (newlist) => {
|
||||
const temp = newlist.map((item) => {
|
||||
// 从 outputs 扁平数组提取 URL
|
||||
const files = item.outputs?.map(o => o.url) || []
|
||||
const files = item.outputs?.map((o) => o.url) || []
|
||||
const request = item.request || {}
|
||||
const generateData = {
|
||||
model: item.model_name || '',
|
||||
@ -152,7 +152,7 @@ const conversion = (newlist) => {
|
||||
customHight: request.customHight,
|
||||
duration: request.duration || '',
|
||||
videoPattern: request.videoPattern || '',
|
||||
modelParams: { ...request },
|
||||
modelParams: { ...request }
|
||||
}
|
||||
// 将 API status 映射为 UI 展示状态
|
||||
let uiStatus = 'success'
|
||||
@ -168,7 +168,7 @@ const conversion = (newlist) => {
|
||||
generateData,
|
||||
time: item.created_at || '',
|
||||
files,
|
||||
collectStatus: item.collectStatus || {},
|
||||
collectStatus: item.collectStatus || {}
|
||||
}
|
||||
})
|
||||
return temp
|
||||
@ -235,7 +235,6 @@ const fetchHistory = async (isLoadMore = false) => {
|
||||
}
|
||||
|
||||
hasMoreData.value = dataList.length === 10
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取历史失败:', error)
|
||||
ElMessage({
|
||||
@ -380,5 +379,4 @@ onBeforeUnmount(() => {
|
||||
background-color: #ccc;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
<script setup>
|
||||
import { computed, ref, onMounted, watch } from 'vue'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import display from './display/index.vue'
|
||||
import { useDisplayStore } from '@/stores'
|
||||
import display from './display/index.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const useDisplay = useDisplayStore()
|
||||
|
||||
@ -4,14 +4,14 @@
|
||||
<div class="icon-wrapper">
|
||||
<div class="lock-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 2C9.243 2 7 4.243 7 7V10H6C4.897 10 4 10.897 4 12V20C4 21.103 4.897 22 6 22H18C19.103 22 20 21.103 20 20V12C20 10.897 19.103 10 18 10H17V7C17 4.243 14.757 2 12 2ZM12 4C13.654 4 15 5.346 15 7V10H9V7C9 5.346 10.346 4 12 4ZM6 12H18V20H6V12Z" fill="currentColor"/>
|
||||
<path d="M12 2C9.243 2 7 4.243 7 7V10H6C4.897 10 4 10.897 4 12V20C4 21.103 4.897 22 6 22H18C19.103 22 20 21.103 20 20V12C20 10.897 19.103 10 18 10H17V7C17 4.243 14.757 2 12 2ZM12 4C13.654 4 15 5.346 15 7V10H9V7C9 5.346 10.346 4 12 4ZM6 12H18V20H6V12Z" fill="currentColor" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<h1 class="title">账号未登录</h1>
|
||||
<p class="description">请先登录以访问完整功能</p>
|
||||
|
||||
|
||||
<div class="action-buttons">
|
||||
<button class="btn-primary" @click="handleLogin">
|
||||
立即登录
|
||||
@ -20,7 +20,7 @@
|
||||
注册账号
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="features">
|
||||
<div class="feature-item">
|
||||
<div class="feature-icon">✨</div>
|
||||
@ -36,7 +36,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="background-decoration">
|
||||
<div class="circle circle-1"></div>
|
||||
<div class="circle circle-2"></div>
|
||||
@ -311,15 +311,15 @@ const handleRegister = () => {
|
||||
.login-content {
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
|
||||
.title {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
|
||||
.action-buttons {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
|
||||
.features {
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user