chore: 代码格式化统一(空格、换行、属性排序、LF规范化)

This commit is contained in:
王佑琳 2026-06-09 18:26:37 +08:00
parent b964c826ce
commit e98ff3a2c4
26 changed files with 501 additions and 509 deletions

View File

@ -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/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 --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 --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 *)"
] ]
} }
} }

View File

@ -43,5 +43,5 @@ export const getUserInfo = () => {
} }
export const checkUsertoken = () => { export const checkUsertoken = () => {
return service.post(`/login/validateToken`) return service.post('/login/validateToken')
} }

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="custom-popover" ref="popoverRef"> <div ref="popoverRef" class="custom-popover">
<div class="popover-trigger" ref="triggerRef" @click.stop="togglePopover"> <div ref="triggerRef" class="popover-trigger" @click.stop="togglePopover">
<slot name="reference" /> <slot name="reference" />
</div> </div>
<Teleport to="body"> <Teleport to="body">
@ -143,10 +143,10 @@ const handleClickOutside = (e) => {
const contentEl = contentRef.value const contentEl = contentRef.value
if ( if (
triggerEl && triggerEl
!triggerEl.contains(e.target) && && !triggerEl.contains(e.target)
contentEl && && contentEl
!contentEl.contains(e.target) && !contentEl.contains(e.target)
) { ) {
visible.value = false visible.value = false
window.__currentOpenPopoverId__ = null window.__currentOpenPopoverId__ = null

View File

@ -377,7 +377,7 @@ onBeforeUnmount(() => {
.option-group { .option-group {
/* margin-bottom: 10px; */ /* margin-bottom: 10px; */
&:last-child { &:last-child {
margin-bottom: 0; margin-bottom: 0;
} }

View File

@ -9,8 +9,8 @@
</div> </div>
<div class="close-btn" @click="handleClose"> <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"> <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="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="M10.6846 1.02413L0.985354 11.0147" stroke="#666666" stroke-width="2" stroke-linecap="round" />
</svg> </svg>
</div> </div>
</div> </div>
@ -32,8 +32,8 @@
ref="editableDivRef" ref="editableDivRef"
contenteditable="true" contenteditable="true"
class="custom-textarea" class="custom-textarea"
@input="handleInput"
:data-placeholder="!inputText ? '请输入提示词或使用圆形/矩形工具' : ''" :data-placeholder="!inputText ? '请输入提示词或使用圆形/矩形工具' : ''"
@input="handleInput"
></div> ></div>
</div> </div>
</div> </div>
@ -48,7 +48,7 @@
@click="currentShape = 'rectangle'" @click="currentShape = 'rectangle'"
> >
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none"> <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> </svg>
</div> </div>
@ -59,7 +59,7 @@
@click="currentShape = 'circle'" @click="currentShape = 'circle'"
> >
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none"> <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> </svg>
</div> </div>
</div> </div>
@ -68,22 +68,22 @@
<!-- 上一步 --> <!-- 上一步 -->
<div class="shape-btn" @click="undo"> <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"> <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> </svg>
</div> </div>
<!-- 下一步 --> <!-- 下一步 -->
<div class="shape-btn" @click="redo"> <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"> <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> </svg>
</div> </div>
<!-- 删除 --> <!-- 删除 -->
<div class="shape-btn" @click="deleteShape"> <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"> <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="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="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> </svg>
</div> </div>
</div> </div>
@ -103,8 +103,8 @@
<span class="brush-panel-title">请输入替换内容描述</span> <span class="brush-panel-title">请输入替换内容描述</span>
<div class="brush-panel-close" @click="closeBrushPanel"> <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"> <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="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="M10.6846 1.02411L0.985354 11.0147" stroke="#666666" stroke-width="2" stroke-linecap="round" />
</svg> </svg>
</div> </div>
</div> </div>
@ -114,8 +114,8 @@
ref="brushTextareaRef" ref="brushTextareaRef"
contenteditable="true" contenteditable="true"
class="brush-textarea" class="brush-textarea"
@input="handleBrushInput"
:data-placeholder="!currentShapeDescription ? '请输入描述...' : ''" :data-placeholder="!currentShapeDescription ? '请输入描述...' : ''"
@input="handleBrushInput"
></div> ></div>
</div> </div>
@ -135,7 +135,7 @@
<img :src="img" alt="参考图" /> <img :src="img" alt="参考图" />
<div class="reference-image-delete" @click.stop="removeReferenceImage(index)"> <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"> <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> </svg>
</div> </div>
</div> </div>
@ -145,7 +145,7 @@
@click="handleUploadReference" @click="handleUploadReference"
> >
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 15 15" fill="none"> <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> </svg>
</div> </div>
</div> </div>
@ -160,10 +160,10 @@
</template> </template>
<script setup> <script setup>
import { generate } from '@/utils/taskPolling'
import { useDisplayStore } from '@/stores' import { useDisplayStore } from '@/stores'
import request from '@/utils/request'
import { getModelId } from '@/utils/modelApi' import { getModelId } from '@/utils/modelApi'
import request from '@/utils/request'
import { generate } from '@/utils/taskPolling'
const props = defineProps({ const props = defineProps({
visible: { visible: {
@ -240,11 +240,11 @@ const updateCurrentEditingContent = (description) => {
const colorIndex = currentEditingShapeIndex.value >= 0 ? currentEditingShapeIndex.value : 0 const colorIndex = currentEditingShapeIndex.value >= 0 ? currentEditingShapeIndex.value : 0
const colorName = colorNames[colorIndex % colorNames.length] const colorName = colorNames[colorIndex % colorNames.length]
const isFirstShape = shapes.value.length === 0 || (currentEditingShapeIndex.value === 0 && shapes.value.length === 1) const isFirstShape = shapes.value.length === 0 || (currentEditingShapeIndex.value === 0 && shapes.value.length === 1)
const currentShape = currentEditingShapeIndex.value >= 0 ? shapes.value[currentEditingShapeIndex.value] : null const currentShape = currentEditingShapeIndex.value >= 0 ? shapes.value[currentEditingShapeIndex.value] : null
const shapeType = currentShape ? currentShape.type : 'rectangle' const shapeType = currentShape ? currentShape.type : 'rectangle'
const shapeWord = shapeType === 'circle' ? '圈' : '框' const shapeWord = shapeType === 'circle' ? '圈' : '框'
if (selectedReferenceImages.value.length > 0) { if (selectedReferenceImages.value.length > 0) {
const imageIndex = allReferenceImages.value.indexOf(selectedReferenceImages.value[0]) + 2 const imageIndex = allReferenceImages.value.indexOf(selectedReferenceImages.value[0]) + 2
const prefix = isFirstShape ? '' : '' const prefix = isFirstShape ? '' : ''
@ -268,7 +268,7 @@ watch(() => props.visible, (newVal) => {
historyIndex.value = -1 historyIndex.value = -1
promptHistory.value = [] promptHistory.value = []
promptHistoryIndex.value = -1 promptHistoryIndex.value = -1
allReferenceImages.value = props.referenceImages.map(img => img.url || img) allReferenceImages.value = props.referenceImages.map((img) => img.url || img)
brushPanelVisible.value = false brushPanelVisible.value = false
isPanelOpen.value = false isPanelOpen.value = false
currentShapeDescription.value = '' currentShapeDescription.value = ''
@ -284,19 +284,19 @@ watch(() => props.visible, (newVal) => {
const initCanvas = () => { const initCanvas = () => {
const canvas = canvasRef.value const canvas = canvasRef.value
if (!canvas) return if (!canvas) return
const ctx = canvas.getContext('2d') const ctx = canvas.getContext('2d')
const container = canvas.parentElement const container = canvas.parentElement
const containerWidth = container.clientWidth const containerWidth = container.clientWidth
const containerHeight = container.clientHeight const containerHeight = container.clientHeight
if (currentImage.value) { if (currentImage.value) {
let imageUrl = currentImage.value let imageUrl = currentImage.value
if (!imageUrl.startsWith('data:')) { if (!imageUrl.startsWith('data:')) {
imageUrl = imageUrl.replace('https://sxwz.xueai.art', 'https://talkingdraw.xueai.art') imageUrl = imageUrl.replace('https://sxwz.xueai.art', 'https://talkingdraw.xueai.art')
} }
if (imageUrl.startsWith('data:')) { if (imageUrl.startsWith('data:')) {
const img = new Image() const img = new Image()
img.onload = () => { img.onload = () => {
@ -310,8 +310,8 @@ const initCanvas = () => {
img.src = imageUrl img.src = imageUrl
} else { } else {
fetch(imageUrl) fetch(imageUrl)
.then(res => res.blob()) .then((res) => res.blob())
.then(blob => { .then((blob) => {
const img = new Image() const img = new Image()
img.onload = () => { img.onload = () => {
const imgScale = Math.min(containerWidth / img.width, containerHeight / img.height) const imgScale = Math.min(containerWidth / img.width, containerHeight / img.height)
@ -363,7 +363,7 @@ const handleMouseDown = (e) => {
const handleMouseMove = (e) => { const handleMouseMove = (e) => {
if (!isDrawing.value) return if (!isDrawing.value) return
const canvas = canvasRef.value const canvas = canvasRef.value
const ctx = canvas.getContext('2d') const ctx = canvas.getContext('2d')
const ratio = canvas.width / canvas.offsetWidth const ratio = canvas.width / canvas.offsetWidth
@ -373,15 +373,15 @@ const handleMouseMove = (e) => {
const savedStartY = startY.value const savedStartY = startY.value
const savedCurrentX = currentX const savedCurrentX = currentX
const savedCurrentY = currentY const savedCurrentY = currentY
if (bgImage.value) { if (bgImage.value) {
ctx.clearRect(0, 0, canvas.width, canvas.height) ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.drawImage(bgImage.value, 0, 0) ctx.drawImage(bgImage.value, 0, 0)
shapes.value.forEach(shape => { shapes.value.forEach((shape) => {
drawShape(ctx, shape) drawShape(ctx, shape)
}) })
const currentShapeData = { const currentShapeData = {
type: currentShape.value, type: currentShape.value,
startX: savedStartX, startX: savedStartX,
@ -396,10 +396,10 @@ const handleMouseMove = (e) => {
ctx.fillStyle = '#fff' ctx.fillStyle = '#fff'
ctx.fillRect(0, 0, canvas.width, canvas.height) ctx.fillRect(0, 0, canvas.width, canvas.height)
shapes.value.forEach(shape => { shapes.value.forEach((shape) => {
drawShape(ctx, shape) drawShape(ctx, shape)
}) })
const currentShapeData = { const currentShapeData = {
type: currentShape.value, type: currentShape.value,
startX: startX.value, startX: startX.value,
@ -414,34 +414,34 @@ const handleMouseMove = (e) => {
const handleMouseUp = (e) => { const handleMouseUp = (e) => {
if (!isDrawing.value) return if (!isDrawing.value) return
const canvas = canvasRef.value const canvas = canvasRef.value
const ratio = canvas.width / canvas.offsetWidth const ratio = canvas.width / canvas.offsetWidth
const endX = e.offsetX * ratio const endX = e.offsetX * ratio
const endY = e.offsetY * ratio const endY = e.offsetY * ratio
if (shapes.value.length >= maxShapes) { if (shapes.value.length >= maxShapes) {
ElMessage.warning('最多只能画5笔') ElMessage.warning('最多只能画5笔')
isDrawing.value = false isDrawing.value = false
return return
} }
const colorIndex = shapes.value.length const colorIndex = shapes.value.length
const newShape = { const newShape = {
type: currentShape.value, type: currentShape.value,
startX: startX.value, startX: startX.value,
startY: startY.value, startY: startY.value,
endX: endX, endX,
endY: endY, endY,
color: shapeColors[colorIndex], color: shapeColors[colorIndex],
description: '', description: '',
referenceImages: [] referenceImages: []
} }
shapes.value.push(newShape) shapes.value.push(newShape)
currentEditingShapeIndex.value = shapes.value.length - 1 currentEditingShapeIndex.value = shapes.value.length - 1
isDrawing.value = false isDrawing.value = false
brushPanelVisible.value = true brushPanelVisible.value = true
isPanelOpen.value = true isPanelOpen.value = true
currentShapeDescription.value = '' currentShapeDescription.value = ''
@ -452,7 +452,7 @@ const saveHistory = () => {
history.value = history.value.slice(0, historyIndex.value + 1) history.value = history.value.slice(0, historyIndex.value + 1)
history.value.push([...shapes.value]) history.value.push([...shapes.value])
historyIndex.value = history.value.length - 1 historyIndex.value = history.value.length - 1
promptHistory.value = promptHistory.value.slice(0, promptHistoryIndex.value + 1) promptHistory.value = promptHistory.value.slice(0, promptHistoryIndex.value + 1)
promptHistory.value.push(inputText.value) promptHistory.value.push(inputText.value)
promptHistoryIndex.value = promptHistory.value.length - 1 promptHistoryIndex.value = promptHistory.value.length - 1
@ -463,7 +463,7 @@ const undo = () => {
historyIndex.value-- historyIndex.value--
shapes.value = historyIndex.value >= 0 ? [...history.value[historyIndex.value]] : [] shapes.value = historyIndex.value >= 0 ? [...history.value[historyIndex.value]] : []
redrawCanvas() redrawCanvas()
promptHistoryIndex.value-- promptHistoryIndex.value--
inputText.value = promptHistoryIndex.value >= 0 ? promptHistory.value[promptHistoryIndex.value] : '' inputText.value = promptHistoryIndex.value >= 0 ? promptHistory.value[promptHistoryIndex.value] : ''
if (editableDivRef.value) { if (editableDivRef.value) {
@ -477,7 +477,7 @@ const redo = () => {
historyIndex.value++ historyIndex.value++
shapes.value = [...history.value[historyIndex.value]] shapes.value = [...history.value[historyIndex.value]]
redrawCanvas() redrawCanvas()
promptHistoryIndex.value++ promptHistoryIndex.value++
inputText.value = promptHistory.value[promptHistoryIndex.value] inputText.value = promptHistory.value[promptHistoryIndex.value]
if (editableDivRef.value) { if (editableDivRef.value) {
@ -493,11 +493,11 @@ const deleteShape = () => {
promptHistoryIndex.value = -1 promptHistoryIndex.value = -1
history.value = [] history.value = []
historyIndex.value = -1 historyIndex.value = -1
if (editableDivRef.value) { if (editableDivRef.value) {
editableDivRef.value.innerHTML = '' editableDivRef.value.innerHTML = ''
} }
redrawCanvas() redrawCanvas()
} }
@ -505,17 +505,17 @@ const redrawCanvas = () => {
const canvas = canvasRef.value const canvas = canvasRef.value
if (!canvas) return if (!canvas) return
const ctx = canvas.getContext('2d') const ctx = canvas.getContext('2d')
ctx.clearRect(0, 0, canvas.width, canvas.height) ctx.clearRect(0, 0, canvas.width, canvas.height)
if (bgImage.value) { if (bgImage.value) {
ctx.drawImage(bgImage.value, 0, 0) ctx.drawImage(bgImage.value, 0, 0)
} else { } else {
ctx.fillStyle = '#fff' ctx.fillStyle = '#fff'
ctx.fillRect(0, 0, canvas.width, canvas.height) ctx.fillRect(0, 0, canvas.width, canvas.height)
} }
shapes.value.forEach(shape => { shapes.value.forEach((shape) => {
drawShape(ctx, shape) drawShape(ctx, shape)
}) })
} }
@ -523,14 +523,14 @@ const redrawCanvas = () => {
const drawShape = (ctx, shape) => { const drawShape = (ctx, shape) => {
ctx.strokeStyle = shape.color || '#ff0000' ctx.strokeStyle = shape.color || '#ff0000'
ctx.lineWidth = 2 ctx.lineWidth = 2
if (shape.type === 'rectangle') { if (shape.type === 'rectangle') {
const width = shape.endX - shape.startX const width = shape.endX - shape.startX
const height = shape.endY - shape.startY const height = shape.endY - shape.startY
ctx.strokeRect(shape.startX, shape.startY, width, height) ctx.strokeRect(shape.startX, shape.startY, width, height)
} else if (shape.type === 'circle') { } else if (shape.type === 'circle') {
const radius = Math.sqrt( 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.beginPath()
ctx.arc(shape.startX, shape.startY, radius, 0, Math.PI * 2) ctx.arc(shape.startX, shape.startY, radius, 0, Math.PI * 2)
@ -553,7 +553,7 @@ const removeReferenceImage = (index) => {
const selectReferenceImage = (index) => { const selectReferenceImage = (index) => {
const img = allReferenceImages.value[index] const img = allReferenceImages.value[index]
const existingIndex = selectedReferenceImages.value.indexOf(img) const existingIndex = selectedReferenceImages.value.indexOf(img)
if (existingIndex !== -1) { if (existingIndex !== -1) {
selectedReferenceImages.value = [] selectedReferenceImages.value = []
} else { } else {
@ -569,14 +569,14 @@ const handleUploadReference = () => {
input.onchange = async (e) => { input.onchange = async (e) => {
const files = Array.from(e.target.files) const files = Array.from(e.target.files)
const remainingSlots = 5 - allReferenceImages.value.length const remainingSlots = 5 - allReferenceImages.value.length
if (remainingSlots <= 0) { if (remainingSlots <= 0) {
ElMessage.warning('最多只能上传5张图片') ElMessage.warning('最多只能上传5张图片')
return return
} }
const filesToUpload = files.slice(0, remainingSlots) const filesToUpload = files.slice(0, remainingSlots)
const readFileAsDataURL = (file) => { const readFileAsDataURL = (file) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const reader = new FileReader() const reader = new FileReader()
@ -585,7 +585,7 @@ const handleUploadReference = () => {
reader.readAsDataURL(file) reader.readAsDataURL(file)
}) })
} }
const imageDataUrls = await Promise.all(filesToUpload.map(readFileAsDataURL)) const imageDataUrls = await Promise.all(filesToUpload.map(readFileAsDataURL))
allReferenceImages.value.push(...imageDataUrls) allReferenceImages.value.push(...imageDataUrls)
} }
@ -597,7 +597,7 @@ const closeBrushPanel = () => {
shapes.value.splice(currentEditingShapeIndex.value, 1) shapes.value.splice(currentEditingShapeIndex.value, 1)
redrawCanvas() redrawCanvas()
} }
brushPanelVisible.value = false brushPanelVisible.value = false
isPanelOpen.value = false isPanelOpen.value = false
currentShapeDescription.value = '' currentShapeDescription.value = ''
@ -611,7 +611,7 @@ const handleBrushConfirm = () => {
shapes.value[currentEditingShapeIndex.value].description = currentShapeDescription.value shapes.value[currentEditingShapeIndex.value].description = currentShapeDescription.value
shapes.value[currentEditingShapeIndex.value].referenceImages = [...selectedReferenceImages.value] shapes.value[currentEditingShapeIndex.value].referenceImages = [...selectedReferenceImages.value]
} }
if (currentEditingContent.value) { if (currentEditingContent.value) {
if (editableDivRef.value) { if (editableDivRef.value) {
const currentContent = editableDivRef.value.innerHTML const currentContent = editableDivRef.value.innerHTML
@ -621,9 +621,9 @@ const handleBrushConfirm = () => {
inputText.value += currentEditingContent.value inputText.value += currentEditingContent.value
} }
} }
saveHistory() saveHistory()
brushPanelVisible.value = false brushPanelVisible.value = false
isPanelOpen.value = false isPanelOpen.value = false
currentShapeDescription.value = '' currentShapeDescription.value = ''
@ -635,10 +635,10 @@ const getImageAspectRatio = () => {
if (!bgImage.value) { if (!bgImage.value) {
return null return null
} }
const width = bgImage.value.width const width = bgImage.value.width
const height = bgImage.value.height const height = bgImage.value.height
const aspectRatios = [ const aspectRatios = [
{ ratio: '4:3', value: 4 / 3 }, { ratio: '4:3', value: 4 / 3 },
{ ratio: '16:9', value: 16 / 9 }, { ratio: '16:9', value: 16 / 9 },
@ -648,12 +648,12 @@ const getImageAspectRatio = () => {
{ ratio: '2:3', value: 2 / 3 }, { ratio: '2:3', value: 2 / 3 },
{ ratio: '3:2', value: 3 / 2 } { ratio: '3:2', value: 3 / 2 }
] ]
const currentRatio = width / height const currentRatio = width / height
let closest = aspectRatios[0] let closest = aspectRatios[0]
let minDiff = Math.abs(currentRatio - closest.value) let minDiff = Math.abs(currentRatio - closest.value)
for (const item of aspectRatios) { for (const item of aspectRatios) {
const diff = Math.abs(currentRatio - item.value) const diff = Math.abs(currentRatio - item.value)
if (diff < minDiff) { if (diff < minDiff) {
@ -661,7 +661,7 @@ const getImageAspectRatio = () => {
closest = item closest = item
} }
} }
return { return {
width, width,
height, height,
@ -675,34 +675,34 @@ const handleSend = async () => {
ElMessage.error('请输入提示词') ElMessage.error('请输入提示词')
return return
} }
isSending.value = true isSending.value = true
try { try {
const canvas = canvasRef.value const canvas = canvasRef.value
const imageData = canvas.toDataURL('image/png') const imageData = canvas.toDataURL('image/png')
const imgs = [] const imgs = []
if (imageData) { if (imageData) {
imgs.push({ name: 'image_1', url: imageData }) imgs.push({ name: 'image_1', url: imageData })
} }
allReferenceImages.value.forEach((img, index) => { allReferenceImages.value.forEach((img, index) => {
imgs.push({ name: `image_${index + 2}`, url: img }) imgs.push({ name: `image_${index + 2}`, url: img })
}) })
const uploadImg = async (imgItem) => { const uploadImg = async (imgItem) => {
if (!imgItem.url.startsWith('data:') && !imgItem.url.startsWith('blob:')) { if (!imgItem.url.startsWith('data:') && !imgItem.url.startsWith('blob:')) {
return imgItem return imgItem
} }
const response = await fetch(imgItem.url) const response = await fetch(imgItem.url)
const blob = await response.blob() const blob = await response.blob()
const file = new File([blob], `${imgItem.name}.png`, { type: 'image/png' }) const file = new File([blob], `${imgItem.name}.png`, { type: 'image/png' })
const formData = new FormData() const formData = new FormData()
formData.append('file', file) formData.append('file', file)
try { try {
const result = await request({ const result = await request({
url: import.meta.env.VITE_API_WORKFLOW_UPLOAD, url: import.meta.env.VITE_API_WORKFLOW_UPLOAD,
@ -721,11 +721,11 @@ const handleSend = async () => {
return imgItem return imgItem
} }
} }
const uploadedImgs = await Promise.all(imgs.map(uploadImg)) const uploadedImgs = await Promise.all(imgs.map(uploadImg))
const proportion = getImageAspectRatio() const proportion = getImageAspectRatio()
const modelId = await getModelId(props.type, 'GPT') const modelId = await getModelId(props.type, 'GPT')
const generateData = { const generateData = {
@ -743,20 +743,20 @@ const handleSend = async () => {
modelId, modelId,
quantity: 1, quantity: 1,
params: [ params: [
{ name: 'prompt', data: inputText.value + '并且去除掉图1中的框' }, { name: 'prompt', data: `${inputText.value}并且去除掉图1中的框` },
{ name: 'index', data: 1 }, { name: 'index', data: 1 },
{ name: 'proportion', data: proportion?.aspectRatio || '4:3' }, { name: 'proportion', data: proportion?.aspectRatio || '4:3' }
], ],
imgs: uploadedImgs, imgs: uploadedImgs,
request: JSON.stringify(generateData) request: JSON.stringify(generateData)
} }
emit('send', { emit('send', {
image: imageData, image: imageData,
text: inputText.value, text: inputText.value,
shapes: shapes.value shapes: shapes.value
}) })
await generate(data, generateData) await generate(data, generateData)
handleClose() handleClose()
} finally { } finally {

View File

@ -1,6 +1,6 @@
<template> <template>
<Transition name="slide-up"> <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 v-if="!props.isGenerate" class="title">{{ platform.label }}</div>
<div class="sender-top"> <div class="sender-top">
@ -11,8 +11,8 @@
<div v-show="showUploader" class="upload-img-container"> <div v-show="showUploader" class="upload-img-container">
<div class="reference-diagram"> <div class="reference-diagram">
<component <component
v-if="platform.ImageUploader"
:is="platform.ImageUploader" :is="platform.ImageUploader"
v-if="platform.ImageUploader"
ref="referenceDiagramRef" ref="referenceDiagramRef"
v-model="referenceImages" v-model="referenceImages"
v-bind="uploaderBindings" v-bind="uploaderBindings"
@ -34,11 +34,11 @@
<div class="prefix-self-wrap"> <div class="prefix-self-wrap">
<component <component
:is="platform.ModelSelector" :is="platform.ModelSelector"
:modelValue="platform.model.value" :model-value="platform.model.value"
@update:modelValue="platform.model.value = $event" :type-value="platform.modelType.value"
:typeValue="platform.modelType.value"
@update:typeValue="platform.modelType.value = $event"
v-bind="(platform.modelSelectorProps && platform.modelSelectorProps()) || {}" 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"> <template v-for="ctrl in visibleControls" :key="ctrl.name">
<component <component
@ -62,18 +62,17 @@
</div> </div>
</template> </template>
</Sender> </Sender>
</div>
</div>
</Transition> </Transition>
</template> </template>
<script setup> <script setup>
import { Sender } from 'vue-element-plus-x' import { Sender } from 'vue-element-plus-x'
import { useDisplayStore } from '@/stores'
import { generate } from '@/utils/taskPolling'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { createPlatform } from '@/platforms/registry.js' import { createPlatform } from '@/platforms/registry.js'
import { useDisplayStore } from '@/stores'
import { getModelId } from '@/utils/modelApi' import { getModelId } from '@/utils/modelApi'
import { generate } from '@/utils/taskPolling'
// //
import '@/platforms/painting/index.js' import '@/platforms/painting/index.js'
@ -82,7 +81,7 @@ import '@/platforms/video/index.js'
const props = defineProps({ const props = defineProps({
isGenerate: { type: Boolean, default: false }, isGenerate: { type: Boolean, default: false },
generate: { type: Boolean, default: false }, generate: { type: Boolean, default: false },
type: { type: String, default: 'Painting' }, type: { type: String, default: 'Painting' }
}) })
const router = useRouter() const router = useRouter()
@ -99,7 +98,7 @@ const getCurrentConfig = () => {
const visibleControls = computed(() => { const visibleControls = computed(() => {
const config = getCurrentConfig() const config = getCurrentConfig()
return platform.value.controls.filter(c => c.show(config)) return platform.value.controls.filter((c) => c.show(config))
}) })
const showUploader = computed(() => { const showUploader = computed(() => {
@ -148,7 +147,7 @@ const handleStart = async () => {
modelType: p.modelType.value, modelType: p.modelType.value,
prompt: prompt.value, prompt: prompt.value,
referenceImages: referenceImages.value, referenceImages: referenceImages.value,
modelParams: body, modelParams: body
} }
const data = { const data = {
@ -157,7 +156,7 @@ const handleStart = async () => {
modelName: p.model.value, modelName: p.model.value,
modelId: modelId || '', modelId: modelId || '',
body, body,
request: JSON.stringify(generateData), request: JSON.stringify(generateData)
} }
await generate(data, generateData) await generate(data, generateData)
@ -190,7 +189,7 @@ watch(
async ([newModel, newModelType]) => { async ([newModel, newModelType]) => {
if (!newModel) return if (!newModel) return
await platform.value.loadConfig(newModel, newModelType) await platform.value.loadConfig(newModel, newModelType)
}, }
) )
// + // +

View File

@ -100,6 +100,8 @@ const props = defineProps({
} }
}) })
const emit = defineEmits(['scroll', 'scroll-start', 'scroll-end', 'visible-change'])
const computedData = computed(() => { const computedData = computed(() => {
return props.data.length > 0 ? props.data : props.items return props.data.length > 0 ? props.data : props.items
}) })
@ -114,8 +116,6 @@ const computedBuffer = computed(() => {
return props.buffer !== 3 ? props.buffer : props.bufferSize return props.buffer !== 3 ? props.buffer : props.bufferSize
}) })
const emit = defineEmits(['scroll', 'scroll-start', 'scroll-end', 'visible-change'])
const containerRef = ref(null) const containerRef = ref(null)
const wrapperRef = ref(null) const wrapperRef = ref(null)
const renderContainerRef = ref(null) const renderContainerRef = ref(null)
@ -140,27 +140,27 @@ const containerStyle = computed(() => {
const totalHeight = computed(() => { const totalHeight = computed(() => {
let height = 0 let height = 0
const len = computedData.value.length const len = computedData.value.length
for (let i = 0; i < len; i++) { for (let i = 0; i < len; i++) {
const cachedHeight = itemHeights.value.get(i) const cachedHeight = itemHeights.value.get(i)
height += cachedHeight !== undefined ? cachedHeight : props.estimatedHeight height += cachedHeight !== undefined ? cachedHeight : props.estimatedHeight
} }
height += props.bottomPlaceholderHeight height += props.bottomPlaceholderHeight
return height return height
}) })
const getItemPosition = (index) => { const getItemPosition = (index) => {
let offset = 0 let offset = 0
for (let i = 0; i < index; i++) { for (let i = 0; i < index; i++) {
const cachedHeight = itemHeights.value.get(i) const cachedHeight = itemHeights.value.get(i)
offset += cachedHeight !== undefined ? cachedHeight : props.estimatedHeight offset += cachedHeight !== undefined ? cachedHeight : props.estimatedHeight
} }
const height = itemHeights.value.get(index) ?? props.estimatedHeight const height = itemHeights.value.get(index) ?? props.estimatedHeight
return { offset, height } return { offset, height }
} }
@ -173,73 +173,73 @@ const visibleRange = computed(() => {
if (!renderContainerRef.value || computedData.value.length === 0) { if (!renderContainerRef.value || computedData.value.length === 0) {
return { start: 0, end: 0, offset: 0 } return { start: 0, end: 0, offset: 0 }
} }
const viewportHeight = containerHeight.value const viewportHeight = containerHeight.value
const currentScrollTop = scrollTop.value const currentScrollTop = scrollTop.value
const bufferCount = computedBuffer.value const bufferCount = computedBuffer.value
let startIndex = 0 let startIndex = 0
let endIndex = computedData.value.length - 1 let endIndex = computedData.value.length - 1
let startOffset = 0 let startOffset = 0
let offset = 0 let offset = 0
for (let i = 0; i < computedData.value.length; i++) { for (let i = 0; i < computedData.value.length; i++) {
const cachedHeight = itemHeights.value.get(i) const cachedHeight = itemHeights.value.get(i)
const height = cachedHeight !== undefined ? cachedHeight : props.estimatedHeight const height = cachedHeight !== undefined ? cachedHeight : props.estimatedHeight
if (offset + height > currentScrollTop) { if (offset + height > currentScrollTop) {
startIndex = Math.max(0, i - bufferCount) startIndex = Math.max(0, i - bufferCount)
break break
} }
offset += height offset += height
} }
startOffset = 0 startOffset = 0
for (let i = 0; i < startIndex; i++) { for (let i = 0; i < startIndex; i++) {
const cachedHeight = itemHeights.value.get(i) const cachedHeight = itemHeights.value.get(i)
startOffset += cachedHeight !== undefined ? cachedHeight : props.estimatedHeight startOffset += cachedHeight !== undefined ? cachedHeight : props.estimatedHeight
} }
offset = startOffset offset = startOffset
endIndex = startIndex endIndex = startIndex
for (let i = startIndex; i < computedData.value.length; i++) { for (let i = startIndex; i < computedData.value.length; i++) {
const cachedHeight = itemHeights.value.get(i) const cachedHeight = itemHeights.value.get(i)
const height = cachedHeight !== undefined ? cachedHeight : props.estimatedHeight const height = cachedHeight !== undefined ? cachedHeight : props.estimatedHeight
offset += height offset += height
if (offset > currentScrollTop + viewportHeight) { if (offset > currentScrollTop + viewportHeight) {
endIndex = Math.min(computedData.value.length - 1, i + bufferCount) endIndex = Math.min(computedData.value.length - 1, i + bufferCount)
break break
} }
endIndex = i endIndex = i
} }
return { start: Math.min(startIndex, endIndex), end: Math.max(startIndex, endIndex), offset: startOffset } return { start: Math.min(startIndex, endIndex), end: Math.max(startIndex, endIndex), offset: startOffset }
}) })
const visibleItems = computed(() => { const visibleItems = computed(() => {
const { start, end, offset } = visibleRange.value const { start, end, offset } = visibleRange.value
const items = [] const items = []
let currentOffset = offset let currentOffset = offset
for (let i = start; i <= end && i < computedData.value.length; i++) { for (let i = start; i <= end && i < computedData.value.length; i++) {
const cachedHeight = itemHeights.value.get(i) const cachedHeight = itemHeights.value.get(i)
const height = cachedHeight !== undefined ? cachedHeight : props.estimatedHeight const height = cachedHeight !== undefined ? cachedHeight : props.estimatedHeight
items.push({ items.push({
item: computedData.value[i], item: computedData.value[i],
index: i, index: i,
offset: currentOffset + props.bottomPlaceholderHeight, offset: currentOffset + props.bottomPlaceholderHeight,
height height
}) })
currentOffset += height currentOffset += height
} }
return items return items
}) })
@ -275,7 +275,7 @@ const bottomPlaceholderStyle = computed(() => ({
top: 0, top: 0,
width: '100%', width: '100%',
height: `${props.bottomPlaceholderHeight}px`, height: `${props.bottomPlaceholderHeight}px`,
transform: `translateY(0px)`, transform: 'translateY(0px)',
zIndex: 1 zIndex: 1
})) }))
@ -312,12 +312,12 @@ const setItemRef = (el, index) => {
const measureItem = (index, element) => { const measureItem = (index, element) => {
if (!element) return if (!element) return
const firstChild = element.firstElementChild const firstChild = element.firstElementChild
const targetElement = firstChild || element const targetElement = firstChild || element
const height = Math.ceil(targetElement.offsetHeight) const height = Math.ceil(targetElement.offsetHeight)
if (height > 0) { if (height > 0) {
const cachedHeight = itemHeights.value.get(index) const cachedHeight = itemHeights.value.get(index)
if (cachedHeight !== height) { if (cachedHeight !== height) {
@ -332,10 +332,10 @@ const setupResizeObserver = () => {
if (resizeObserver.value) { if (resizeObserver.value) {
resizeObserver.value.disconnect() resizeObserver.value.disconnect()
} }
resizeObserver.value = new ResizeObserver((entries) => { resizeObserver.value = new ResizeObserver((entries) => {
for (const entry of 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)) { if (!isNaN(index)) {
measureItem(index, entry.target) measureItem(index, entry.target)
} }
@ -345,15 +345,15 @@ const setupResizeObserver = () => {
const handleWheel = (event) => { const handleWheel = (event) => {
if (!renderContainerRef.value) return if (!renderContainerRef.value) return
const { deltaY } = event const { deltaY } = event
const el = renderContainerRef.value const el = renderContainerRef.value
el.scrollBy({ el.scrollBy({
top: -deltaY, top: -deltaY,
behavior: 'instant' behavior: 'instant'
}) })
event.preventDefault() event.preventDefault()
} }
@ -363,15 +363,15 @@ const handleScroll = (event) => {
const target = event.target const target = event.target
scrollTop.value = target.scrollTop scrollTop.value = target.scrollTop
isScrolling.value = true isScrolling.value = true
if (scrollTimeout.value) { if (scrollTimeout.value) {
clearTimeout(scrollTimeout.value) clearTimeout(scrollTimeout.value)
} }
scrollTimeout.value = setTimeout(() => { scrollTimeout.value = setTimeout(() => {
isScrolling.value = false isScrolling.value = false
}, 150) }, 150)
// ,100ms // ,100ms
if (scrollCleanupTimeout.value) { if (scrollCleanupTimeout.value) {
clearTimeout(scrollCleanupTimeout.value) clearTimeout(scrollCleanupTimeout.value)
@ -379,19 +379,19 @@ const handleScroll = (event) => {
scrollCleanupTimeout.value = setTimeout(() => { scrollCleanupTimeout.value = setTimeout(() => {
cleanupExtraItems(visibleItems.value) cleanupExtraItems(visibleItems.value)
}, 300) }, 300)
const st = target.scrollTop const st = target.scrollTop
const scrollHeight = target.scrollHeight const scrollHeight = target.scrollHeight
const clientHeight = target.clientHeight const clientHeight = target.clientHeight
const distanceToContainerTop = st const distanceToContainerTop = st
const distanceToContainerBottom = scrollHeight - st - clientHeight const distanceToContainerBottom = scrollHeight - st - clientHeight
const distanceToPageTop = distanceToContainerBottom const distanceToPageTop = distanceToContainerBottom
const distanceToPageBottom = distanceToContainerTop const distanceToPageBottom = distanceToContainerTop
const isAtPageTop = distanceToPageTop <= 0 const isAtPageTop = distanceToPageTop <= 0
const isAtPageBottom = distanceToPageBottom <= 0 const isAtPageBottom = distanceToPageBottom <= 0
emit('scroll', { emit('scroll', {
target, target,
scrollTop: st, scrollTop: st,
@ -402,11 +402,11 @@ const handleScroll = (event) => {
isAtPageTop, isAtPageTop,
isAtPageBottom isAtPageBottom
}) })
if (isAtPageTop) { if (isAtPageTop) {
emit('scroll-start') emit('scroll-start')
} }
if (isAtPageBottom) { if (isAtPageBottom) {
emit('scroll-end') emit('scroll-end')
} }
@ -414,9 +414,9 @@ const handleScroll = (event) => {
const scrollToIndex = (index, behavior = 'auto') => { const scrollToIndex = (index, behavior = 'auto') => {
if (!renderContainerRef.value || index < 0 || index >= computedData.value.length) return if (!renderContainerRef.value || index < 0 || index >= computedData.value.length) return
const position = getItemPosition(index) const position = getItemPosition(index)
renderContainerRef.value.scrollTo({ renderContainerRef.value.scrollTo({
top: position.offset, top: position.offset,
behavior behavior
@ -428,10 +428,10 @@ const scrollToBottom = (behavior = 'smooth') => {
pendingScrollToBottom.value = true pendingScrollToBottom.value = true
return return
} }
requestAnimationFrame(() => { requestAnimationFrame(() => {
if (!renderContainerRef.value) return if (!renderContainerRef.value) return
renderContainerRef.value.scrollTo({ renderContainerRef.value.scrollTo({
top: 0, top: 0,
behavior behavior
@ -441,10 +441,10 @@ const scrollToBottom = (behavior = 'smooth') => {
const scrollToTop = (behavior = 'smooth') => { const scrollToTop = (behavior = 'smooth') => {
if (!renderContainerRef.value) return if (!renderContainerRef.value) return
requestAnimationFrame(() => { requestAnimationFrame(() => {
if (!renderContainerRef.value) return if (!renderContainerRef.value) return
const scrollHeight = renderContainerRef.value.scrollHeight const scrollHeight = renderContainerRef.value.scrollHeight
renderContainerRef.value.scrollTo({ renderContainerRef.value.scrollTo({
top: scrollHeight, top: scrollHeight,
@ -479,9 +479,9 @@ const isAtPageTop = () => {
const observeVisibleItems = () => { const observeVisibleItems = () => {
if (!resizeObserver.value) return if (!resizeObserver.value) return
resizeObserver.value.disconnect() resizeObserver.value.disconnect()
for (const [index, element] of itemRefs) { for (const [index, element] of itemRefs) {
if (element) { if (element) {
resizeObserver.value.observe(element) resizeObserver.value.observe(element)
@ -492,24 +492,24 @@ const observeVisibleItems = () => {
watch(() => computedData.value, (newData, oldData) => { watch(() => computedData.value, (newData, oldData) => {
const oldLength = oldData?.length || 0 const oldLength = oldData?.length || 0
const newLength = newData.length const newLength = newData.length
if (newLength !== oldLength) { if (newLength !== oldLength) {
const newHeights = new Map() const newHeights = new Map()
const minLen = Math.min(oldLength, newLength) const minLen = Math.min(oldLength, newLength)
for (let i = 0; i < minLen; i++) { for (let i = 0; i < minLen; i++) {
if (itemHeights.value.has(i)) { if (itemHeights.value.has(i)) {
newHeights.set(i, itemHeights.value.get(i)) newHeights.set(i, itemHeights.value.get(i))
} }
} }
itemHeights.value = newHeights itemHeights.value = newHeights
nextTick(() => { nextTick(() => {
observeVisibleItems() observeVisibleItems()
}) })
} }
previousDataLength.value = newLength previousDataLength.value = newLength
}, { deep: false }) }, { deep: false })
@ -527,36 +527,36 @@ watch(visibleItems, (newItems) => {
const cleanupExtraItems = (currentVisibleItems) => { const cleanupExtraItems = (currentVisibleItems) => {
if (!renderContainerRef.value || !currentVisibleItems.length) return 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 // render-container .virtual-scroller-item
const renderedItems = renderContainerRef.value.querySelectorAll('.virtual-scroller-item') const renderedItems = renderContainerRef.value.querySelectorAll('.virtual-scroller-item')
const toRemove = [] const toRemove = []
for (const el of renderedItems) { for (const el of renderedItems) {
const dataIndex = parseInt(el.getAttribute('data-index'), 10) const dataIndex = Number.parseInt(el.getAttribute('data-index'), 10)
// data-index , // data-index ,
if (!isNaN(dataIndex) && !visibleIndices.has(dataIndex)) { if (!isNaN(dataIndex) && !visibleIndices.has(dataIndex)) {
toRemove.push(el) toRemove.push(el)
} }
} }
// DOM // DOM
for (const el of toRemove) { for (const el of toRemove) {
if (el.parentNode) { if (el.parentNode) {
el.parentNode.removeChild(el) el.parentNode.removeChild(el)
} }
// itemRefs Map // itemRefs Map
const index = parseInt(el.getAttribute('data-index'), 10) const index = Number.parseInt(el.getAttribute('data-index'), 10)
if (!isNaN(index)) { if (!isNaN(index)) {
itemRefs.delete(index) itemRefs.delete(index)
} }
} }
if (toRemove.length > 0) { if (toRemove.length > 0) {
console.log(`[VirtualScroller] 清理了 ${toRemove.length} 个多余DOM元素`) console.log(`[VirtualScroller] 清理了 ${toRemove.length} 个多余DOM元素`)
} }
@ -566,13 +566,13 @@ onMounted(() => {
setupResizeObserver() setupResizeObserver()
isInitialized.value = true isInitialized.value = true
previousDataLength.value = computedData.value.length previousDataLength.value = computedData.value.length
nextTick(() => { nextTick(() => {
if (pendingScrollToBottom.value) { if (pendingScrollToBottom.value) {
pendingScrollToBottom.value = false pendingScrollToBottom.value = false
scrollToBottom() scrollToBottom()
} }
observeVisibleItems() observeVisibleItems()
cleanupExtraItems(visibleItems.value) cleanupExtraItems(visibleItems.value)
}) })
@ -582,15 +582,15 @@ onBeforeUnmount(() => {
if (resizeObserver.value) { if (resizeObserver.value) {
resizeObserver.value.disconnect() resizeObserver.value.disconnect()
} }
if (scrollTimeout.value) { if (scrollTimeout.value) {
clearTimeout(scrollTimeout.value) clearTimeout(scrollTimeout.value)
} }
if (scrollCleanupTimeout.value) { if (scrollCleanupTimeout.value) {
clearTimeout(scrollCleanupTimeout.value) clearTimeout(scrollCleanupTimeout.value)
} }
itemRefs.clear() itemRefs.clear()
}) })
@ -611,56 +611,56 @@ defineExpose({
<style lang="less" scoped> <style lang="less" scoped>
.virtual-scroller { .virtual-scroller {
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
&::-webkit-scrollbar { &::-webkit-scrollbar {
display: none; display: none;
width: 6px; width: 6px;
height: 6px; height: 6px;
} }
&::-webkit-scrollbar-track { &::-webkit-scrollbar-track {
background: transparent; background: transparent;
} }
&::-webkit-scrollbar-thumb { &::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.2); background: rgba(0, 0, 0, 0.2);
border-radius: 3px; border-radius: 3px;
&:hover { &:hover {
background: rgba(0, 0, 0, 0.3); background: rgba(0, 0, 0, 0.3);
} }
} }
.virtual-scroller-wrapper { .virtual-scroller-wrapper {
contain: content; contain: content;
} }
.virtual-scroller-spacer { .virtual-scroller-spacer {
flex-shrink: 0; flex-shrink: 0;
width: 100%; width: 100%;
} }
.virtual-scroller-placeholder { .virtual-scroller-placeholder {
width: 100%; width: 100%;
} }
.virtual-scroller-render-container { .virtual-scroller-render-container {
contain: layout style; contain: layout style;
&::-webkit-scrollbar { &::-webkit-scrollbar {
display: none; display: none;
width: 6px; width: 6px;
height: 6px; height: 6px;
} }
} }
.virtual-scroller-item { .virtual-scroller-item {
contain: layout style; contain: layout style;
backface-visibility: hidden; backface-visibility: hidden;
perspective: 1000px; perspective: 1000px;
} }
.virtual-scroller-bottom-placeholder { .virtual-scroller-bottom-placeholder {
contain: layout style; contain: layout style;
} }

View File

@ -99,6 +99,8 @@ const props = defineProps({
} }
}) })
const emit = defineEmits(['scroll', 'scroll-start', 'scroll-end', 'visible-change'])
const computedData = computed(() => { const computedData = computed(() => {
return props.data.length > 0 ? props.data : props.items return props.data.length > 0 ? props.data : props.items
}) })
@ -113,8 +115,6 @@ const computedBuffer = computed(() => {
return props.buffer !== 3 ? props.buffer : props.bufferSize return props.buffer !== 3 ? props.buffer : props.bufferSize
}) })
const emit = defineEmits(['scroll', 'scroll-start', 'scroll-end', 'visible-change'])
const containerRef = ref(null) const containerRef = ref(null)
const wrapperRef = ref(null) const wrapperRef = ref(null)
const renderContainerRef = ref(null) const renderContainerRef = ref(null)
@ -136,27 +136,27 @@ const totalHeight = computed(() => {
heightVersion.value // measureItem mutate Map heightVersion.value // measureItem mutate Map
let height = 0 let height = 0
const len = computedData.value.length const len = computedData.value.length
for (let i = 0; i < len; i++) { for (let i = 0; i < len; i++) {
const cachedHeight = itemHeights.value.get(i) const cachedHeight = itemHeights.value.get(i)
height += cachedHeight !== undefined ? cachedHeight : props.estimatedHeight height += cachedHeight !== undefined ? cachedHeight : props.estimatedHeight
} }
height += props.bottomPlaceholderHeight height += props.bottomPlaceholderHeight
return height return height
}) })
const getItemPosition = (index) => { const getItemPosition = (index) => {
let offset = 0 let offset = 0
for (let i = 0; i < index; i++) { for (let i = 0; i < index; i++) {
const cachedHeight = itemHeights.value.get(i) const cachedHeight = itemHeights.value.get(i)
offset += cachedHeight !== undefined ? cachedHeight : props.estimatedHeight offset += cachedHeight !== undefined ? cachedHeight : props.estimatedHeight
} }
const height = itemHeights.value.get(index) ?? props.estimatedHeight const height = itemHeights.value.get(index) ?? props.estimatedHeight
return { offset, height } return { offset, height }
} }
@ -218,23 +218,23 @@ const visibleRange = computed(() => {
const visibleItems = computed(() => { const visibleItems = computed(() => {
const { start, end, offset } = visibleRange.value const { start, end, offset } = visibleRange.value
const items = [] const items = []
let currentOffset = offset let currentOffset = offset
for (let i = start; i <= end && i < computedData.value.length; i++) { for (let i = start; i <= end && i < computedData.value.length; i++) {
const cachedHeight = itemHeights.value.get(i) const cachedHeight = itemHeights.value.get(i)
const height = cachedHeight !== undefined ? cachedHeight : props.estimatedHeight const height = cachedHeight !== undefined ? cachedHeight : props.estimatedHeight
items.push({ items.push({
item: computedData.value[i], item: computedData.value[i],
index: i, index: i,
offset: currentOffset + props.bottomPlaceholderHeight, offset: currentOffset + props.bottomPlaceholderHeight,
height height
}) })
currentOffset += height currentOffset += height
} }
return items return items
}) })
@ -269,7 +269,7 @@ const bottomPlaceholderStyle = computed(() => ({
top: 0, top: 0,
width: '100%', width: '100%',
height: `${props.bottomPlaceholderHeight}px`, height: `${props.bottomPlaceholderHeight}px`,
transform: `translateY(0px)`, transform: 'translateY(0px)',
zIndex: 1 zIndex: 1
})) }))
@ -344,10 +344,10 @@ const setupResizeObserver = () => {
if (resizeObserver.value) { if (resizeObserver.value) {
resizeObserver.value.disconnect() resizeObserver.value.disconnect()
} }
resizeObserver.value = new ResizeObserver((entries) => { resizeObserver.value = new ResizeObserver((entries) => {
for (const entry of 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)) { if (!isNaN(index)) {
measureItem(index, entry.target) measureItem(index, entry.target)
} }
@ -357,15 +357,15 @@ const setupResizeObserver = () => {
const handleWheel = (event) => { const handleWheel = (event) => {
if (!renderContainerRef.value) return if (!renderContainerRef.value) return
const { deltaY } = event const { deltaY } = event
const el = renderContainerRef.value const el = renderContainerRef.value
el.scrollBy({ el.scrollBy({
top: -deltaY, top: -deltaY,
behavior: 'instant' behavior: 'instant'
}) })
event.preventDefault() event.preventDefault()
} }
@ -376,15 +376,15 @@ const handleScroll = (event) => {
const st = target.scrollTop const st = target.scrollTop
const scrollHeight = target.scrollHeight const scrollHeight = target.scrollHeight
const clientHeight = target.clientHeight const clientHeight = target.clientHeight
const distanceToContainerTop = st const distanceToContainerTop = st
const distanceToContainerBottom = scrollHeight - st - clientHeight const distanceToContainerBottom = scrollHeight - st - clientHeight
const distanceToPageTop = distanceToContainerBottom const distanceToPageTop = distanceToContainerBottom
const distanceToPageBottom = distanceToContainerTop const distanceToPageBottom = distanceToContainerTop
const isAtPageTop = distanceToPageTop <= 0 const isAtPageTop = distanceToPageTop <= 0
const isAtPageBottom = distanceToPageBottom <= 0 const isAtPageBottom = distanceToPageBottom <= 0
emit('scroll', { emit('scroll', {
target, target,
scrollTop: st, scrollTop: st,
@ -395,11 +395,11 @@ const handleScroll = (event) => {
isAtPageTop, isAtPageTop,
isAtPageBottom isAtPageBottom
}) })
if (isAtPageTop) { if (isAtPageTop) {
emit('scroll-start') emit('scroll-start')
} }
if (isAtPageBottom) { if (isAtPageBottom) {
emit('scroll-end') emit('scroll-end')
} }
@ -407,9 +407,9 @@ const handleScroll = (event) => {
const scrollToIndex = (index, behavior = 'auto') => { const scrollToIndex = (index, behavior = 'auto') => {
if (!renderContainerRef.value || index < 0 || index >= computedData.value.length) return if (!renderContainerRef.value || index < 0 || index >= computedData.value.length) return
const position = getItemPosition(index) const position = getItemPosition(index)
renderContainerRef.value.scrollTo({ renderContainerRef.value.scrollTo({
top: position.offset, top: position.offset,
behavior behavior
@ -421,10 +421,10 @@ const scrollToBottom = (behavior = 'smooth') => {
pendingScrollToBottom.value = true pendingScrollToBottom.value = true
return return
} }
requestAnimationFrame(() => { requestAnimationFrame(() => {
if (!renderContainerRef.value) return if (!renderContainerRef.value) return
renderContainerRef.value.scrollTo({ renderContainerRef.value.scrollTo({
top: 0, top: 0,
behavior behavior
@ -434,10 +434,10 @@ const scrollToBottom = (behavior = 'smooth') => {
const scrollToTop = (behavior = 'smooth') => { const scrollToTop = (behavior = 'smooth') => {
if (!renderContainerRef.value) return if (!renderContainerRef.value) return
requestAnimationFrame(() => { requestAnimationFrame(() => {
if (!renderContainerRef.value) return if (!renderContainerRef.value) return
const scrollHeight = renderContainerRef.value.scrollHeight const scrollHeight = renderContainerRef.value.scrollHeight
renderContainerRef.value.scrollTo({ renderContainerRef.value.scrollTo({
top: scrollHeight, top: scrollHeight,
@ -552,56 +552,56 @@ defineExpose({
<style lang="less" scoped> <style lang="less" scoped>
.virtual-scroller { .virtual-scroller {
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
&::-webkit-scrollbar { &::-webkit-scrollbar {
display: none; display: none;
width: 6px; width: 6px;
height: 6px; height: 6px;
} }
&::-webkit-scrollbar-track { &::-webkit-scrollbar-track {
background: transparent; background: transparent;
} }
&::-webkit-scrollbar-thumb { &::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.2); background: rgba(0, 0, 0, 0.2);
border-radius: 3px; border-radius: 3px;
&:hover { &:hover {
background: rgba(0, 0, 0, 0.3); background: rgba(0, 0, 0, 0.3);
} }
} }
.virtual-scroller-wrapper { .virtual-scroller-wrapper {
contain: content; contain: content;
} }
.virtual-scroller-spacer { .virtual-scroller-spacer {
flex-shrink: 0; flex-shrink: 0;
width: 100%; width: 100%;
} }
.virtual-scroller-placeholder { .virtual-scroller-placeholder {
width: 100%; width: 100%;
} }
.virtual-scroller-render-container { .virtual-scroller-render-container {
contain: layout style; contain: layout style;
&::-webkit-scrollbar { &::-webkit-scrollbar {
display: none; display: none;
width: 6px; width: 6px;
height: 6px; height: 6px;
} }
} }
.virtual-scroller-item { .virtual-scroller-item {
contain: layout style; contain: layout style;
backface-visibility: hidden; backface-visibility: hidden;
perspective: 1000px; perspective: 1000px;
} }
.virtual-scroller-bottom-placeholder { .virtual-scroller-bottom-placeholder {
contain: layout style; contain: layout style;
} }

View File

@ -1,194 +1,194 @@
window.TEST_DATA = [ window.TEST_DATA = [
{ {
"id": "839217090555557410", id: '839217090555557410',
"fileUrl": "http://test.xueai.art/file/2026/4/27/69ef43d23888d39e4ed1d062.png", fileUrl: 'http://test.xueai.art/file/2026/4/27/69ef43d23888d39e4ed1d062.png',
"prompt": "这是一幅治愈写实风格的插画画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶空调外机挂在墙边金属防护栏环绕四周外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅旁边还有小凳。彩色水桶一个是红色一个是蓝色放置在角落花盆里栽种着金桔水仙花、蝴蝶兰整齐排列大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树绿意浓郁与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼整个场景被绿意环绕明亮柔和的光线洒下充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格", prompt: '这是一幅治愈写实风格的插画画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶空调外机挂在墙边金属防护栏环绕四周外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅旁边还有小凳。彩色水桶一个是红色一个是蓝色放置在角落花盆里栽种着金桔水仙花、蝴蝶兰整齐排列大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树绿意浓郁与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼整个场景被绿意环绕明亮柔和的光线洒下充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格',
"model": "flux" model: 'flux'
}, },
{ {
"id": "839211834861958673", id: '839211834861958673',
"fileUrl": "http://test.xueai.art/file/2026/4/27/69ef3eed3888d39e4ed1d061.png", fileUrl: 'http://test.xueai.art/file/2026/4/27/69ef3eed3888d39e4ed1d061.png',
"prompt": "这是一幅治愈写实风格的插画画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶空调外机挂在墙边金属防护栏环绕四周外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅旁边还有小凳。彩色水桶一个是红色一个是蓝色放置在角落花盆里栽种着金桔水仙花、蝴蝶兰整齐排列大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树绿意浓郁与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼整个场景被绿意环绕明亮柔和的光线洒下充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格", prompt: '这是一幅治愈写实风格的插画画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶空调外机挂在墙边金属防护栏环绕四周外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅旁边还有小凳。彩色水桶一个是红色一个是蓝色放置在角落花盆里栽种着金桔水仙花、蝴蝶兰整齐排列大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树绿意浓郁与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼整个场景被绿意环绕明亮柔和的光线洒下充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格',
"model": "BananaPro" model: 'BananaPro'
}, },
{ {
"id": "839209605929121287", id: '839209605929121287',
"fileUrl": "http://test.xueai.art/file/2026/4/27/69ef3cd93888d39e4ed1d060.png", fileUrl: 'http://test.xueai.art/file/2026/4/27/69ef3cd93888d39e4ed1d060.png',
"prompt": "这是一幅治愈写实风格的插画画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶空调外机挂在墙边金属防护栏环绕四周外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅旁边还有小凳。彩色水桶一个是红色一个是蓝色放置在角落花盆里栽种着金桔水仙花、蝴蝶兰整齐排列大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树绿意浓郁与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼整个场景被绿意环绕明亮柔和的光线洒下充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格", prompt: '这是一幅治愈写实风格的插画画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶空调外机挂在墙边金属防护栏环绕四周外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅旁边还有小凳。彩色水桶一个是红色一个是蓝色放置在角落花盆里栽种着金桔水仙花、蝴蝶兰整齐排列大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树绿意浓郁与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼整个场景被绿意环绕明亮柔和的光线洒下充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格',
"model": "flux" model: 'flux'
}, },
{ {
"id": "837370053866304564", id: '837370053866304564',
"fileUrl": "http://test.xueai.art/file/2026/4/22/69e88ba23888d39e4ed1d05c.png", fileUrl: 'http://test.xueai.art/file/2026/4/22/69e88ba23888d39e4ed1d05c.png',
"prompt": "这是一幅治愈写实风格的插画画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶空调外机挂在墙边金属防护栏环绕四周外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅旁边还有小凳。彩色水桶一个是红色一个是蓝色放置在角落花盆里栽种着金桔水仙花、蝴蝶兰整齐排列大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树绿意浓郁与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼整个场景被绿意环绕明亮柔和的光线洒下充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格", prompt: '这是一幅治愈写实风格的插画画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶空调外机挂在墙边金属防护栏环绕四周外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅旁边还有小凳。彩色水桶一个是红色一个是蓝色放置在角落花盆里栽种着金桔水仙花、蝴蝶兰整齐排列大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树绿意浓郁与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼整个场景被绿意环绕明亮柔和的光线洒下充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格',
"model": "flux" model: 'flux'
}, },
{ {
"id": "837360015437214709", id: '837360015437214709',
"fileUrl": "http://test.xueai.art/file/2026/4/22/69e882493888d39e4ed1d05b.png", fileUrl: 'http://test.xueai.art/file/2026/4/22/69e882493888d39e4ed1d05b.png',
"prompt": "这是一幅治愈写实风格的插画画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶空调外机挂在墙边金属防护栏环绕四周外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅旁边还有小凳。彩色水桶一个是红色一个是蓝色放置在角落花盆里栽种着金桔水仙花、蝴蝶兰整齐排列大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树绿意浓郁与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼整个场景被绿意环绕明亮柔和的光线洒下充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格", prompt: '这是一幅治愈写实风格的插画画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶空调外机挂在墙边金属防护栏环绕四周外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅旁边还有小凳。彩色水桶一个是红色一个是蓝色放置在角落花盆里栽种着金桔水仙花、蝴蝶兰整齐排列大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树绿意浓郁与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼整个场景被绿意环绕明亮柔和的光线洒下充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格',
"model": "flux" model: 'flux'
}, },
{ {
"id": "836534979084169461", id: '836534979084169461',
"fileUrl": "http://test.xueai.art/file/2026/4/20/69e581e93888d39e4ed1d025.png", 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>", 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" model: 'banana'
}, },
{ {
"id": "835464458670191734", id: '835464458670191734',
"fileUrl": "http://test.xueai.art/file/2026/4/17/69e19ce93888d39e4ed1cfaf.png", fileUrl: 'http://test.xueai.art/file/2026/4/17/69e19ce93888d39e4ed1cfaf.png',
"prompt": "这是一幅治愈写实风格的插画画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶空调外机挂在墙边金属防护栏环绕四周外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅旁边还有小凳。彩色水桶一个是红色一个是蓝色放置在角落花盆里栽种着金桔水仙花、蝴蝶兰整齐排列大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树绿意浓郁与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼整个场景被绿意环绕明亮柔和的光线洒下充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格", prompt: '这是一幅治愈写实风格的插画画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶空调外机挂在墙边金属防护栏环绕四周外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅旁边还有小凳。彩色水桶一个是红色一个是蓝色放置在角落花盆里栽种着金桔水仙花、蝴蝶兰整齐排列大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树绿意浓郁与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼整个场景被绿意环绕明亮柔和的光线洒下充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格',
"model": "flux" model: 'flux'
}, },
{ {
"id": "835463648116749398", id: '835463648116749398',
"fileUrl": "http://test.xueai.art/file/2026/4/17/69e19c273888d39e4ed1cfac.png", fileUrl: 'http://test.xueai.art/file/2026/4/17/69e19c273888d39e4ed1cfac.png',
"prompt": "这是一幅治愈写实风格的插画画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶空调外机挂在墙边金属防护栏环绕四周外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅旁边还有小凳。彩色水桶一个是红色一个是蓝色放置在角落花盆里栽种着金桔水仙花、蝴蝶兰整齐排列大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树绿意浓郁与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼整个场景被绿意环绕明亮柔和的光线洒下充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格", prompt: '这是一幅治愈写实风格的插画画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶空调外机挂在墙边金属防护栏环绕四周外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅旁边还有小凳。彩色水桶一个是红色一个是蓝色放置在角落花盆里栽种着金桔水仙花、蝴蝶兰整齐排列大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树绿意浓郁与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼整个场景被绿意环绕明亮柔和的光线洒下充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格',
"model": "flux" model: 'flux'
}, },
{ {
"id": "835463392293565520", id: '835463392293565520',
"fileUrl": "http://test.xueai.art/file/2026/4/17/69e19bea3888d39e4ed1cfab.png", fileUrl: 'http://test.xueai.art/file/2026/4/17/69e19bea3888d39e4ed1cfab.png',
"prompt": "这是一幅治愈写实风格的插画画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶空调外机挂在墙边金属防护栏环绕四周外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅旁边还有小凳。彩色水桶一个是红色一个是蓝色放置在角落花盆里栽种着金桔水仙花、蝴蝶兰整齐排列大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树绿意浓郁与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼整个场景被绿意环绕明亮柔和的光线洒下充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格", prompt: '这是一幅治愈写实风格的插画画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶空调外机挂在墙边金属防护栏环绕四周外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅旁边还有小凳。彩色水桶一个是红色一个是蓝色放置在角落花盆里栽种着金桔水仙花、蝴蝶兰整齐排列大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树绿意浓郁与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼整个场景被绿意环绕明亮柔和的光线洒下充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格',
"model": "flux" model: 'flux'
}, },
{ {
"id": "832562717234575283", 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", 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)风格", prompt: '这是一幅治愈写实风格的插画画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶空调外机挂在墙边金属防护栏环绕四周外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅旁边还有小凳。彩色水桶一个是红色一个是蓝色放置在角落花盆里栽种着金桔水仙花、蝴蝶兰整齐排列大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树绿意浓郁与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼整个场景被绿意环绕明亮柔和的光线洒下充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格',
"model": "flux" model: 'flux'
}, },
{ {
"id": "839217090555557410", id: '839217090555557410',
"fileUrl": "http://test.xueai.art/file/2026/4/27/69ef43d23888d39e4ed1d062.png", fileUrl: 'http://test.xueai.art/file/2026/4/27/69ef43d23888d39e4ed1d062.png',
"prompt": "这是一幅治愈写实风格的插画画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶空调外机挂在墙边金属防护栏环绕四周外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅旁边还有小凳。彩色水桶一个是红色一个是蓝色放置在角落花盆里栽种着金桔水仙花、蝴蝶兰整齐排列大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树绿意浓郁与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼整个场景被绿意环绕明亮柔和的光线洒下充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格", prompt: '这是一幅治愈写实风格的插画画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶空调外机挂在墙边金属防护栏环绕四周外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅旁边还有小凳。彩色水桶一个是红色一个是蓝色放置在角落花盆里栽种着金桔水仙花、蝴蝶兰整齐排列大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树绿意浓郁与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼整个场景被绿意环绕明亮柔和的光线洒下充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格',
"model": "flux" model: 'flux'
}, },
{ {
"id": "839211834861958673", id: '839211834861958673',
"fileUrl": "http://test.xueai.art/file/2026/4/27/69ef3eed3888d39e4ed1d061.png", fileUrl: 'http://test.xueai.art/file/2026/4/27/69ef3eed3888d39e4ed1d061.png',
"prompt": "这是一幅治愈写实风格的插画画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶空调外机挂在墙边金属防护栏环绕四周外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅旁边还有小凳。彩色水桶一个是红色一个是蓝色放置在角落花盆里栽种着金桔水仙花、蝴蝶兰整齐排列大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树绿意浓郁与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼整个场景被绿意环绕明亮柔和的光线洒下充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格", prompt: '这是一幅治愈写实风格的插画画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶空调外机挂在墙边金属防护栏环绕四周外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅旁边还有小凳。彩色水桶一个是红色一个是蓝色放置在角落花盆里栽种着金桔水仙花、蝴蝶兰整齐排列大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树绿意浓郁与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼整个场景被绿意环绕明亮柔和的光线洒下充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格',
"model": "BananaPro" model: 'BananaPro'
}, },
{ {
"id": "839209605929121287", id: '839209605929121287',
"fileUrl": "http://test.xueai.art/file/2026/4/27/69ef3cd93888d39e4ed1d060.png", fileUrl: 'http://test.xueai.art/file/2026/4/27/69ef3cd93888d39e4ed1d060.png',
"prompt": "这是一幅治愈写实风格的插画画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶空调外机挂在墙边金属防护栏环绕四周外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅旁边还有小凳。彩色水桶一个是红色一个是蓝色放置在角落花盆里栽种着金桔水仙花、蝴蝶兰整齐排列大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树绿意浓郁与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼整个场景被绿意环绕明亮柔和的光线洒下充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格", prompt: '这是一幅治愈写实风格的插画画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶空调外机挂在墙边金属防护栏环绕四周外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅旁边还有小凳。彩色水桶一个是红色一个是蓝色放置在角落花盆里栽种着金桔水仙花、蝴蝶兰整齐排列大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树绿意浓郁与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼整个场景被绿意环绕明亮柔和的光线洒下充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格',
"model": "flux" model: 'flux'
}, },
{ {
"id": "837370053866304564", id: '837370053866304564',
"fileUrl": "http://test.xueai.art/file/2026/4/22/69e88ba23888d39e4ed1d05c.png", fileUrl: 'http://test.xueai.art/file/2026/4/22/69e88ba23888d39e4ed1d05c.png',
"prompt": "这是一幅治愈写实风格的插画画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶空调外机挂在墙边金属防护栏环绕四周外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅旁边还有小凳。彩色水桶一个是红色一个是蓝色放置在角落花盆里栽种着金桔水仙花、蝴蝶兰整齐排列大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树绿意浓郁与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼整个场景被绿意环绕明亮柔和的光线洒下充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格", prompt: '这是一幅治愈写实风格的插画画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶空调外机挂在墙边金属防护栏环绕四周外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅旁边还有小凳。彩色水桶一个是红色一个是蓝色放置在角落花盆里栽种着金桔水仙花、蝴蝶兰整齐排列大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树绿意浓郁与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼整个场景被绿意环绕明亮柔和的光线洒下充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格',
"model": "flux" model: 'flux'
}, },
{ {
"id": "837360015437214709", id: '837360015437214709',
"fileUrl": "http://test.xueai.art/file/2026/4/22/69e882493888d39e4ed1d05b.png", fileUrl: 'http://test.xueai.art/file/2026/4/22/69e882493888d39e4ed1d05b.png',
"prompt": "这是一幅治愈写实风格的插画画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶空调外机挂在墙边金属防护栏环绕四周外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅旁边还有小凳。彩色水桶一个是红色一个是蓝色放置在角落花盆里栽种着金桔水仙花、蝴蝶兰整齐排列大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树绿意浓郁与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼整个场景被绿意环绕明亮柔和的光线洒下充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格", prompt: '这是一幅治愈写实风格的插画画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶空调外机挂在墙边金属防护栏环绕四周外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅旁边还有小凳。彩色水桶一个是红色一个是蓝色放置在角落花盆里栽种着金桔水仙花、蝴蝶兰整齐排列大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树绿意浓郁与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼整个场景被绿意环绕明亮柔和的光线洒下充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格',
"model": "flux" model: 'flux'
}, },
{ {
"id": "836534979084169461", id: '836534979084169461',
"fileUrl": "http://test.xueai.art/file/2026/4/20/69e581e93888d39e4ed1d025.png", 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>", 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" model: 'banana'
}, },
{ {
"id": "835464458670191734", id: '835464458670191734',
"fileUrl": "http://test.xueai.art/file/2026/4/17/69e19ce93888d39e4ed1cfaf.png", fileUrl: 'http://test.xueai.art/file/2026/4/17/69e19ce93888d39e4ed1cfaf.png',
"prompt": "这是一幅治愈写实风格的插画画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶空调外机挂在墙边金属防护栏环绕四周外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅旁边还有小凳。彩色水桶一个是红色一个是蓝色放置在角落花盆里栽种着金桔水仙花、蝴蝶兰整齐排列大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树绿意浓郁与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼整个场景被绿意环绕明亮柔和的光线洒下充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格", prompt: '这是一幅治愈写实风格的插画画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶空调外机挂在墙边金属防护栏环绕四周外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅旁边还有小凳。彩色水桶一个是红色一个是蓝色放置在角落花盆里栽种着金桔水仙花、蝴蝶兰整齐排列大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树绿意浓郁与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼整个场景被绿意环绕明亮柔和的光线洒下充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格',
"model": "flux" model: 'flux'
}, },
{ {
"id": "835463648116749398", id: '835463648116749398',
"fileUrl": "http://test.xueai.art/file/2026/4/17/69e19c273888d39e4ed1cfac.png", fileUrl: 'http://test.xueai.art/file/2026/4/17/69e19c273888d39e4ed1cfac.png',
"prompt": "这是一幅治愈写实风格的插画画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶空调外机挂在墙边金属防护栏环绕四周外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅旁边还有小凳。彩色水桶一个是红色一个是蓝色放置在角落花盆里栽种着金桔水仙花、蝴蝶兰整齐排列大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树绿意浓郁与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼整个场景被绿意环绕明亮柔和的光线洒下充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格", prompt: '这是一幅治愈写实风格的插画画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶空调外机挂在墙边金属防护栏环绕四周外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅旁边还有小凳。彩色水桶一个是红色一个是蓝色放置在角落花盆里栽种着金桔水仙花、蝴蝶兰整齐排列大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树绿意浓郁与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼整个场景被绿意环绕明亮柔和的光线洒下充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格',
"model": "flux" model: 'flux'
}, },
{ {
"id": "835463392293565520", id: '835463392293565520',
"fileUrl": "http://test.xueai.art/file/2026/4/17/69e19bea3888d39e4ed1cfab.png", fileUrl: 'http://test.xueai.art/file/2026/4/17/69e19bea3888d39e4ed1cfab.png',
"prompt": "这是一幅治愈写实风格的插画画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶空调外机挂在墙边金属防护栏环绕四周外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅旁边还有小凳。彩色水桶一个是红色一个是蓝色放置在角落花盆里栽种着金桔水仙花、蝴蝶兰整齐排列大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树绿意浓郁与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼整个场景被绿意环绕明亮柔和的光线洒下充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格", prompt: '这是一幅治愈写实风格的插画画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶空调外机挂在墙边金属防护栏环绕四周外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅旁边还有小凳。彩色水桶一个是红色一个是蓝色放置在角落花盆里栽种着金桔水仙花、蝴蝶兰整齐排列大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树绿意浓郁与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼整个场景被绿意环绕明亮柔和的光线洒下充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格',
"model": "flux" model: 'flux'
}, },
{ {
"id": "832562717234575283", 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", 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)风格", prompt: '这是一幅治愈写实风格的插画画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶空调外机挂在墙边金属防护栏环绕四周外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅旁边还有小凳。彩色水桶一个是红色一个是蓝色放置在角落花盆里栽种着金桔水仙花、蝴蝶兰整齐排列大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树绿意浓郁与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼整个场景被绿意环绕明亮柔和的光线洒下充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格',
"model": "flux" model: 'flux'
}, },
{ {
"id": "830138209152283808", id: '830138209152283808',
"fileUrl": "http://test.xueai.art/file/2026/4/2/69ce3c743888d39e4ed1cf4f.png", fileUrl: 'http://test.xueai.art/file/2026/4/2/69ce3c743888d39e4ed1cf4f.png',
"prompt": "将图1红色框内的【苹果】替换为【火龙果】", prompt: '将图1红色框内的【苹果】替换为【火龙果】',
"model": "banana" model: 'banana'
}, },
{ {
"id": "830136945106498711", id: '830136945106498711',
"fileUrl": "http://test.xueai.art/file/2026/4/2/69ce3b463888d39e4ed1cf4e.png", fileUrl: 'http://test.xueai.art/file/2026/4/2/69ce3b463888d39e4ed1cf4e.png',
"prompt": "一个女孩在树下吃苹果", prompt: '一个女孩在树下吃苹果',
"model": "flux" model: 'flux'
}, },
{ {
"id": "830083758811001839", id: '830083758811001839',
"fileUrl": "http://test.xueai.art/file/2026/4/2/69ce09be3888d39e4ed1cf48.png", fileUrl: 'http://test.xueai.art/file/2026/4/2/69ce09be3888d39e4ed1cf48.png',
"prompt": "一个女孩在树下吃苹果", prompt: '一个女孩在树下吃苹果',
"model": "flux" model: 'flux'
}, },
{ {
"id": "829393290267734386", id: '829393290267734386',
"fileUrl": "http://test.xueai.art/file/2026/3/31/69cb86b13888d39e4ed1cf32.png", fileUrl: 'http://test.xueai.art/file/2026/3/31/69cb86b13888d39e4ed1cf32.png',
"prompt": "将图1红色框内的【女孩】替换为【图2中的女孩】", prompt: '将图1红色框内的【女孩】替换为【图2中的女孩】',
"model": "banana" model: 'banana'
}, },
{ {
"id": "829389466203337022", id: '829389466203337022',
"fileUrl": "http://test.xueai.art/file/2026/3/31/69cb83223888d39e4ed1cf31.png", fileUrl: 'http://test.xueai.art/file/2026/3/31/69cb83223888d39e4ed1cf31.png',
"prompt": "将图1红色框内的【<span style=\"font-size: 14px;\">女孩</span>】替换为【图2中的女孩】", prompt: '将图1红色框内的【<span style="font-size: 14px;">女孩</span>】替换为【图2中的女孩】',
"model": "banana" model: 'banana'
}, },
{ {
"id": "829388114303660338", id: '829388114303660338',
"fileUrl": "http://test.xueai.art/file/2026/3/31/69cb81df3888d39e4ed1cf30.png", fileUrl: 'http://test.xueai.art/file/2026/3/31/69cb81df3888d39e4ed1cf30.png',
"prompt": "将图1红色框内的【女孩】替换为【男孩】", prompt: '将图1红色框内的【女孩】替换为【男孩】',
"model": "banana" model: 'banana'
}, },
{ {
"id": "829381253919682782", id: '829381253919682782',
"fileUrl": "http://test.xueai.art/file/2026/3/31/69cb7b7c3888d39e4ed1cf2d.png", fileUrl: 'http://test.xueai.art/file/2026/3/31/69cb7b7c3888d39e4ed1cf2d.png',
"prompt": "将图1红色框内的【女孩】替换为【图2中的男孩】", prompt: '将图1红色框内的【女孩】替换为【图2中的男孩】',
"model": "banana" model: 'banana'
}, },
{ {
"id": "829324561060212843", id: '829324561060212843',
"fileUrl": "http://test.xueai.art/file/2026/3/31/69cb46af3888d39e4ed1cf29.png", fileUrl: 'http://test.xueai.art/file/2026/3/31/69cb46af3888d39e4ed1cf29.png',
"prompt": "一个女孩在树下吃苹果", prompt: '一个女孩在树下吃苹果',
"model": "flux" model: 'flux'
}, },
{ {
"id": "829319226454978647", 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", 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)风格", prompt: '这是一幅治愈写实风格的插画画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶空调外机挂在墙边金属防护栏环绕四周外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅旁边还有小凳。彩色水桶一个是红色一个是蓝色放置在角落花盆里栽种着金桔水仙花、蝴蝶兰整齐排列大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树绿意浓郁与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼整个场景被绿意环绕明亮柔和的光线洒下充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格',
"model": "flux" model: 'flux'
}, },
{ {
"id": "829317957644464188", id: '829317957644464188',
"fileUrl": "http://test.xueai.art/file/2026/3/31/69cb40893888d39e4ed1cf20.png", fileUrl: 'http://test.xueai.art/file/2026/3/31/69cb40893888d39e4ed1cf20.png',
"prompt": "这是一幅治愈写实风格的插画画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶空调外机挂在墙边金属防护栏环绕四周外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅旁边还有小凳。彩色水桶一个是红色一个是蓝色放置在角落花盆里栽种着金桔水仙花、蝴蝶兰整齐排列大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树绿意浓郁与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼整个场景被绿意环绕明亮柔和的光线洒下充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格", prompt: '这是一幅治愈写实风格的插画画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶空调外机挂在墙边金属防护栏环绕四周外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅旁边还有小凳。彩色水桶一个是红色一个是蓝色放置在角落花盆里栽种着金桔水仙花、蝴蝶兰整齐排列大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树绿意浓郁与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼整个场景被绿意环绕明亮柔和的光线洒下充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格',
"model": "flux" model: 'flux'
}, },
{ {
"id": "829305227994738709", id: '829305227994738709',
"fileUrl": "http://test.xueai.art/file/2026/3/31/69cb34ae3888d39e4ed1cf1e.png", fileUrl: 'http://test.xueai.art/file/2026/3/31/69cb34ae3888d39e4ed1cf1e.png',
"prompt": "这是一幅治愈写实风格的插画画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶空调外机挂在墙边金属防护栏环绕四周外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅旁边还有小凳。彩色水桶一个是红色一个是蓝色放置在角落花盆里栽种着金桔水仙花、蝴蝶兰整齐排列大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树绿意浓郁与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼整个场景被绿意环绕明亮柔和的光线洒下充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格", prompt: '这是一幅治愈写实风格的插画画面中展现了一处城市石砌民居的顶层露台。石砌民居带有木质门、铁栅栏窗台、锈色金属屋顶空调外机挂在墙边金属防护栏环绕四周外置楼梯延伸下来。露台上摆放着竹制躺椅和木质桌椅旁边还有小凳。彩色水桶一个是红色一个是蓝色放置在角落花盆里栽种着金桔水仙花、蝴蝶兰整齐排列大量花卉在露台上盛放。露台旁有一棵枝叶繁茂、遮天蔽日的大树绿意浓郁与建筑共生。周围还摆放着一些绿植盆栽。远处是城市的高楼整个场景被绿意环绕明亮柔和的光线洒下充满了都市生活与自然融合感的顶层露台风情。Moebius (Jean Giraud)风格',
"model": "flux" model: 'flux'
}, },
{ {
"id": "829068575099597628", id: '829068575099597628',
"fileUrl": "http://test.xueai.art/file/2026/3/30/69ca58473888d39e0bb8728b.png", fileUrl: 'http://test.xueai.art/file/2026/3/30/69ca58473888d39e0bb8728b.png',
"prompt": "", prompt: '',
"model": "" model: ''
} }
]; ]

View File

@ -7,8 +7,8 @@
<div class="input-group"> <div class="input-group">
<label>W</label> <label>W</label>
<input <input
type="number"
v-model.number="localWidth" v-model.number="localWidth"
type="number"
:min="minW" :min="minW"
:max="maxW" :max="maxW"
@input="onWidthChange" @input="onWidthChange"
@ -21,8 +21,8 @@
<div class="input-group"> <div class="input-group">
<label>H</label> <label>H</label>
<input <input
type="number"
v-model.number="localHeight" v-model.number="localHeight"
type="number"
:min="minH" :min="minH"
:max="maxH" :max="maxH"
@input="onHeightChange" @input="onHeightChange"
@ -41,9 +41,9 @@
</template> </template>
<script setup> <script setup>
import Popover from '@/components/Popover/index.vue'
import lockIcon from '@/assets/dialog/lock.svg' import lockIcon from '@/assets/dialog/lock.svg'
import lockNoIcon from '@/assets/dialog/lockNo.svg' import lockNoIcon from '@/assets/dialog/lockNo.svg'
import Popover from '@/components/Popover/index.vue'
const props = defineProps({ const props = defineProps({
width: { type: Number, default: 1024 }, width: { type: Number, default: 1024 },
@ -51,7 +51,7 @@ const props = defineProps({
minW: { type: Number, default: 256 }, minW: { type: Number, default: 256 },
maxW: { type: Number, default: 6197 }, maxW: { type: Number, default: 6197 },
minH: { type: Number, default: 256 }, minH: { type: Number, default: 256 },
maxH: { type: Number, default: 4096 }, maxH: { type: Number, default: 4096 }
}) })
const emit = defineEmits(['update:width', 'update:height']) const emit = defineEmits(['update:width', 'update:height'])

View File

@ -37,7 +37,7 @@
<div class="size-inputs"> <div class="size-inputs">
<div class="input-group"> <div class="input-group">
<label>W</label> <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>
<div class="lock-icon" :class="{ locked: isLocked }" @click="toggleLock"> <div class="lock-icon" :class="{ locked: isLocked }" @click="toggleLock">
<img :src="isLocked ? lockIcon : lockNoIcon" alt="约束比例"> <img :src="isLocked ? lockIcon : lockNoIcon" alt="约束比例">
@ -45,7 +45,7 @@
</div> </div>
<div class="input-group"> <div class="input-group">
<label>H</label> <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> </div>
</div> </div>
@ -60,9 +60,9 @@
</template> </template>
<script setup> <script setup>
import Popover from '@/components/Popover/index.vue'
import lockIcon from '@/assets/dialog/lock.svg' import lockIcon from '@/assets/dialog/lock.svg'
import lockNoIcon from '@/assets/dialog/lockNo.svg' import lockNoIcon from '@/assets/dialog/lockNo.svg'
import Popover from '@/components/Popover/index.vue'
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
@ -95,7 +95,7 @@ const props = defineProps({
}, },
allowCustom: { allowCustom: {
type: Boolean, type: Boolean,
default: true, default: true
}, },
resolutionOptions: { resolutionOptions: {
type: Array, type: Array,
@ -461,5 +461,4 @@ watch(() => [props.modelValue, props.resolution], () => {
background: #f5f6f7; background: #f5f6f7;
} }
} }
</style> </style>

View File

@ -19,8 +19,8 @@
</div> </div>
</div> </div>
<el-upload <el-upload
ref="uploadRef"
v-show="false" v-show="false"
ref="uploadRef"
:action="uploadurl" :action="uploadurl"
multiple multiple
:limit="limit" :limit="limit"

View File

@ -15,10 +15,9 @@
import Select from '@/components/Select/index.vue' import Select from '@/components/Select/index.vue'
import { fetchPlatformModels, getPlatformCode } from '@/utils/modelApi' import { fetchPlatformModels, getPlatformCode } from '@/utils/modelApi'
const props = defineProps({ const props = defineProps({
modelValue: { type: String, default: 'Flux 2' }, modelValue: { type: String, default: 'Flux 2' },
typeValue: { type: String, default: 'text' }, typeValue: { type: String, default: 'text' }
}) })
const emit = defineEmits(['update:modelValue', 'update:typeValue']) const emit = defineEmits(['update:modelValue', 'update:typeValue'])
@ -28,7 +27,7 @@ const platformModels = ref([])
const categoryMap = [ const categoryMap = [
{ tag: 'text', label: '生成模型', inputType: 'text' }, { tag: 'text', label: '生成模型', inputType: 'text' },
{ tag: 'edit', label: '编辑模型', inputType: 'image' }, { tag: 'edit', label: '编辑模型', inputType: 'image' },
{ tag: 'vision', label: '视觉理解模型', inputType: 'vision' }, { tag: 'vision', label: '视觉理解模型', inputType: 'vision' }
] ]
function parseValue(encoded) { function parseValue(encoded) {
@ -44,14 +43,14 @@ function encodeValue(tag, modelName) {
function findTagForModel(modelName) { function findTagForModel(modelName) {
for (const cat of categoryMap) { 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 if (model) return cat.tag
} }
return 'text' return 'text'
} }
function tagToInputType(tag) { function tagToInputType(tag) {
const cat = categoryMap.find(c => c.tag === tag) const cat = categoryMap.find((c) => c.tag === tag)
return cat?.inputType || 'text' return cat?.inputType || 'text'
} }
@ -67,7 +66,7 @@ const selectValue = computed({
if (!parsed) return if (!parsed) return
emit('update:modelValue', parsed.modelName) emit('update:modelValue', parsed.modelName)
emit('update:typeValue', tagToInputType(parsed.tag)) emit('update:typeValue', tagToInputType(parsed.tag))
}, }
}) })
// API // API
@ -89,26 +88,26 @@ const modelGroups = computed(() => {
if (models.length === 0) return [] if (models.length === 0) return []
return categoryMap return categoryMap
.filter(cat => models.some(m => m.tags?.includes(cat.tag))) .filter((cat) => models.some((m) => m.tags?.includes(cat.tag)))
.map(cat => ({ .map((cat) => ({
label: cat.label, label: cat.label,
options: models options: models
.filter(m => m.tags?.includes(cat.tag)) .filter((m) => m.tags?.includes(cat.tag))
.map(m => ({ .map((m) => ({
value: `${cat.tag}::${m.display_name || m.name}`, value: `${cat.tag}::${m.display_name || m.name}`,
label: 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) => { watch(platformModels, (models) => {
if (models.length === 0) return 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) { if (!currentModel || currentModel.disabled) {
const firstEnabled = models.find(m => !m.disabled) const firstEnabled = models.find((m) => !m.disabled)
if (firstEnabled) { if (firstEnabled) {
emit('update:modelValue', firstEnabled.display_name || firstEnabled.name) emit('update:modelValue', firstEnabled.display_name || firstEnabled.name)
emit('update:typeValue', tagToInputType(findTagForModel(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 if (!newValue) return
const models = platformModels.value const models = platformModels.value
if (models.length === 0) return 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) { if (currentModel && currentModel.disabled) {
const firstEnabled = models.find(m => !m.disabled) const firstEnabled = models.find((m) => !m.disabled)
if (firstEnabled) { if (firstEnabled) {
emit('update:modelValue', firstEnabled.display_name || firstEnabled.name) emit('update:modelValue', firstEnabled.display_name || firstEnabled.name)
emit('update:typeValue', tagToInputType(findTagForModel(firstEnabled.display_name || firstEnabled.name))) emit('update:typeValue', tagToInputType(findTagForModel(firstEnabled.display_name || firstEnabled.name)))

View File

@ -18,12 +18,12 @@
</template> </template>
<script setup> <script setup>
import Select from '@/components/Select/index.vue'
import videoPattern2 from '@/assets/dialog/videoPattern2.svg' import videoPattern2 from '@/assets/dialog/videoPattern2.svg'
import videoPattern4 from '@/assets/dialog/videoPattern4.svg' import videoPattern4 from '@/assets/dialog/videoPattern4.svg'
import videoPattern5 from '@/assets/dialog/videoPattern5.svg' import videoPattern5 from '@/assets/dialog/videoPattern5.svg'
import videoPattern6 from '@/assets/dialog/videoPattern6.svg' import videoPattern6 from '@/assets/dialog/videoPattern6.svg'
import Select from '@/components/Select/index.vue'
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
@ -44,12 +44,12 @@ const quantityOptions = [
{ value: '首尾帧', label: '首尾帧', labelText: '首尾帧', icon: videoPattern2 }, { value: '首尾帧', label: '首尾帧', labelText: '首尾帧', icon: videoPattern2 },
{ value: '数字人', label: '数字人', labelText: '数字人', icon: videoPattern2 }, { value: '数字人', label: '数字人', labelText: '数字人', icon: videoPattern2 },
{ value: '全能参考', label: '全能参考', labelText: '全能参考', icon: videoPattern4 }, { value: '全能参考', label: '全能参考', labelText: '全能参考', icon: videoPattern4 },
{ value: '智能多帧', label: '智能多帧', labelText: '智能多帧', icon: videoPattern5 }, { value: '智能多帧', label: '智能多帧', labelText: '智能多帧', icon: videoPattern5 },
{ value: '主体参考', label: '主体参考', labelText: '主体参考', icon: videoPattern6 } { value: '主体参考', label: '主体参考', labelText: '主体参考', icon: videoPattern6 }
] ]
const selectedIcon = computed(() => { 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 return option ? option.icon : videoPattern1
}) })
</script> </script>
@ -62,20 +62,20 @@ const selectedIcon = computed(() => {
border-radius: 10px; border-radius: 10px;
border: 1px solid #E8E9EB; border: 1px solid #E8E9EB;
background: #f5f6f7; background: #f5f6f7;
&:hover { &:hover {
background: #e9eaeb; background: #e9eaeb;
} }
} }
:deep(.select-text) { :deep(.select-text) {
font-size: 14px; font-size: 14px;
} }
:deep(.dropdown-menu) { :deep(.dropdown-menu) {
min-width: 140px; min-width: 140px;
} }
:deep(.dropdown-item) { :deep(.dropdown-item) {
min-width: 80px; min-width: 80px;
justify-content: start; justify-content: start;

View File

@ -4,8 +4,8 @@
<div class="section"> <div class="section">
<h3>选择比例</h3> <h3>选择比例</h3>
<div class="proportion-options" :style="{ marginBottom: props.type === 'Video' ? '0px' : '20px' }"> <div class="proportion-options" :style="{ marginBottom: props.type === 'Video' ? '0px' : '20px' }">
<div <div
v-for="item in proportionOptions" v-for="item in proportionOptions"
:key="item.value" :key="item.value"
class="proportion-item" class="proportion-item"
:class="{ active: proportion === item.value }" :class="{ active: proportion === item.value }"
@ -20,8 +20,8 @@
<div class="section"> <div class="section">
<h3>选择分辨率</h3> <h3>选择分辨率</h3>
<div class="resolution-options"> <div class="resolution-options">
<div <div
v-for="item in resolutionOptions" v-for="item in resolutionOptions"
:key="item.value" :key="item.value"
class="resolution-item" class="resolution-item"
:class="{ active: resolution === item.value }" :class="{ active: resolution === item.value }"
@ -107,7 +107,7 @@ const getProportionStyle = (value) => {
const [w, h] = value.split(':').map(Number) const [w, h] = value.split(':').map(Number)
const aspectRatio = w / h const aspectRatio = w / h
const baseSize = 20 const baseSize = 20
if (aspectRatio > 1) { if (aspectRatio > 1) {
return { return {
'--width': `${baseSize}px`, '--width': `${baseSize}px`,
@ -148,11 +148,11 @@ const getProportionStyle = (value) => {
.section{ .section{
margin-bottom: 20px; margin-bottom: 20px;
border-radius: 20px; border-radius: 20px;
&:last-child{ &:last-child{
margin-bottom: 0; margin-bottom: 0;
} }
h3{ h3{
font-family: "Microsoft YaHei"; font-family: "Microsoft YaHei";
font-size: 12px; font-size: 12px;
@ -185,7 +185,7 @@ const getProportionStyle = (value) => {
border-radius: 5px; border-radius: 5px;
text-align: bottom; text-align: bottom;
color: #999; color: #999;
&::before{ &::before{
content: ''; content: '';
width: var(--width, 20px); width: var(--width, 20px);
@ -195,11 +195,11 @@ const getProportionStyle = (value) => {
transition: all 0.2s ease; transition: all 0.2s ease;
border: 2px solid #999; border: 2px solid #999;
} }
&:hover{ &:hover{
background: #e0e0e0; background: #e0e0e0;
} }
&.active{ &.active{
color: #000F33; color: #000F33;
background: #ffffff; background: #ffffff;
@ -228,11 +228,11 @@ const getProportionStyle = (value) => {
text-align: center; text-align: center;
transition: all 0.2s ease; transition: all 0.2s ease;
color: #666; color: #666;
&:hover{ &:hover{
background: #e0e0e0; background: #e0e0e0;
} }
&.active{ &.active{
background: #ffffff; background: #ffffff;
color: #000000; color: #000000;

View File

@ -53,20 +53,20 @@ const quantityOptions = computed(() => props.options)
border-radius: 10px; border-radius: 10px;
border: 1px solid #E8E9EB; border: 1px solid #E8E9EB;
background: #f5f6f7; background: #f5f6f7;
&:hover { &:hover {
background: #e9eaeb; background: #e9eaeb;
} }
} }
:deep(.select-text) { :deep(.select-text) {
font-size: 14px; font-size: 14px;
} }
:deep(.dropdown-menu) { :deep(.dropdown-menu) {
min-width: 136px; min-width: 136px;
} }
:deep(.dropdown-item) { :deep(.dropdown-item) {
min-width: 80px; min-width: 80px;
justify-content: center; justify-content: center;

View File

@ -25,9 +25,9 @@
</div> </div>
<el-upload <el-upload
v-for="i in maxImages" v-for="i in maxImages"
v-show="false"
:key="i" :key="i"
:ref="el => setUploadRef(el, i - 1)" :ref="el => setUploadRef(el, i - 1)"
v-show="false"
:action="uploadurl" :action="uploadurl"
:limit="1" :limit="1"
:before-upload="beforeUpload" :before-upload="beforeUpload"
@ -90,9 +90,9 @@ watch(() => props.modelValue, async (newVal) => {
if (isUploading.value) { if (isUploading.value) {
return return
} }
imageList.value = [...newVal] imageList.value = [...newVal]
const newPreviewList = [] const newPreviewList = []
for (const img of newVal) { for (const img of newVal) {
let previewImg = { ...img } let previewImg = { ...img }
@ -140,16 +140,16 @@ const beforeUpload = (rawFile) => {
const handleSuccess = (response, uploadFile, index) => { const handleSuccess = (response, uploadFile, index) => {
ElMessage.success('上传成功') ElMessage.success('上传成功')
isUploading.value = true isUploading.value = true
const localUrl = URL.createObjectURL(uploadFile.raw) const localUrl = URL.createObjectURL(uploadFile.raw)
const newImage = { const newImage = {
uid: uploadFile.uid, uid: uploadFile.uid,
url: response.url url: response.url
} }
if (imageList.value[index]) { if (imageList.value[index]) {
const previewItem = localPreviewList.value[index] const previewItem = localPreviewList.value[index]
if (previewItem && previewItem.url && previewItem.url.startsWith('blob:')) { if (previewItem && previewItem.url && previewItem.url.startsWith('blob:')) {
@ -169,9 +169,9 @@ const handleSuccess = (response, uploadFile, index) => {
serverUrl: response.url serverUrl: response.url
} }
} }
emit('update:modelValue', [...imageList.value]) emit('update:modelValue', [...imageList.value])
nextTick(() => { nextTick(() => {
isUploading.value = false isUploading.value = false
}) })
@ -186,7 +186,7 @@ const handleDelete = (index) => {
if (previewItem && previewItem.url && previewItem.url.startsWith('blob:')) { if (previewItem && previewItem.url && previewItem.url.startsWith('blob:')) {
URL.revokeObjectURL(previewItem.url) URL.revokeObjectURL(previewItem.url)
} }
localPreviewList.value.splice(index, 1) localPreviewList.value.splice(index, 1)
imageList.value.splice(index, 1) imageList.value.splice(index, 1)
emit('update:modelValue', [...imageList.value]) emit('update:modelValue', [...imageList.value])

View File

@ -44,15 +44,14 @@ const fetchConfig = async () => {
} }
} }
fetchConfig() fetchConfig()
watch(() => videoConfig.value, (newConfig) => { watch(() => videoConfig.value, (newConfig) => {
const models = newConfig[props.videoPattern] || [] const models = newConfig[props.videoPattern] || []
if (models.length > 0) { if (models.length > 0) {
const enabledModels = models.filter(m => !m.disabled) const enabledModels = models.filter((m) => !m.disabled)
if (enabledModels.length > 0) { if (enabledModels.length > 0) {
const currentModelExists = enabledModels.find(m => m.value === props.modelValue) const currentModelExists = enabledModels.find((m) => m.value === props.modelValue)
if (!currentModelExists) { if (!currentModelExists) {
model.value = enabledModels[0].value model.value = enabledModels[0].value
} }
@ -60,7 +59,6 @@ watch(() => videoConfig.value, (newConfig) => {
} }
}, { deep: true }) }, { deep: true })
const model = computed({ const model = computed({
get: () => props.modelValue, get: () => props.modelValue,
set: (value) => { set: (value) => {
@ -91,9 +89,9 @@ const getModelType = (value) => {
watch(() => props.videoPattern, (newPattern) => { watch(() => props.videoPattern, (newPattern) => {
const models = videoConfig.value[newPattern] || [] const models = videoConfig.value[newPattern] || []
if (models.length > 0) { if (models.length > 0) {
const enabledModels = models.filter(m => !m.disabled) const enabledModels = models.filter((m) => !m.disabled)
if (enabledModels.length > 0) { if (enabledModels.length > 0) {
const currentModelExists = enabledModels.find(m => m.value === props.modelValue) const currentModelExists = enabledModels.find((m) => m.value === props.modelValue)
if (!currentModelExists) { if (!currentModelExists) {
model.value = enabledModels[0].value model.value = enabledModels[0].value
} }
@ -103,9 +101,9 @@ watch(() => props.videoPattern, (newPattern) => {
watch(() => props.modelValue, (newValue) => { watch(() => props.modelValue, (newValue) => {
const models = videoConfig.value[props.videoPattern] || [] 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) { if (currentModel && currentModel.disabled) {
const enabledModels = models.filter(m => !m.disabled) const enabledModels = models.filter((m) => !m.disabled)
if (enabledModels.length > 0) { if (enabledModels.length > 0) {
model.value = enabledModels[0].value model.value = enabledModels[0].value
} }
@ -121,24 +119,24 @@ watch(() => props.modelValue, (newValue) => {
border-radius: 10px; border-radius: 10px;
border: 1px solid #E8E9EB; border: 1px solid #E8E9EB;
background: #f5f6f7; background: #f5f6f7;
&:hover { &:hover {
background: #e9eaeb; background: #e9eaeb;
} }
} }
:deep(.select-text) { :deep(.select-text) {
font-size: 14px; font-size: 14px;
} }
:deep(.dropdown-menu) { :deep(.dropdown-menu) {
max-height: 510px; max-height: 510px;
overflow-y: auto; overflow-y: auto;
} }
:deep(.dropdown-item) { :deep(.dropdown-item) {
min-width: 120px; min-width: 120px;
&.active { &.active {
background: rgba(0, 15, 51, 0.10); background: rgba(0, 15, 51, 0.10);
color: #000F33; color: #000F33;

View File

@ -1,5 +1,5 @@
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import { useDisplayStore, useUserStore } from '@/stores' import { useUserStore } from '@/stores'
import { getToken, setToken } from '@/utils/auth' import { getToken, setToken } from '@/utils/auth'
const routes = [ const routes = [
@ -30,7 +30,7 @@ const router = createRouter({
}) })
router.beforeEach(async (to, from) => { router.beforeEach(async (to, from) => {
if(to.query.token){ if (to.query.token) {
setToken(to.query.token) setToken(to.query.token)
} else { } else {
// 检查是否有 token // 检查是否有 token

View File

@ -8,12 +8,12 @@ const DisplayStoreSetup = () => {
const isLoading = ref(false) const isLoading = ref(false)
const currentResultData = ref(null) const currentResultData = ref(null)
const dialogBoxRef = ref(null) const dialogBoxRef = ref(null)
const canvasVisible = ref(false) const canvasVisible = ref(false)
const canvasImage = ref('') const canvasImage = ref('')
const canvasReferenceImages = ref([]) const canvasReferenceImages = ref([])
const canvasSource = ref('') const canvasSource = ref('')
const addGeneratingItem = (item) => { const addGeneratingItem = (item) => {
const newItem = { const newItem = {
id: item.taskId || crypto.randomUUID(), id: item.taskId || crypto.randomUUID(),
@ -26,29 +26,29 @@ const DisplayStoreSetup = () => {
tempList.value.unshift(newItem) tempList.value.unshift(newItem)
return newItem return newItem
} }
const updateItemToSuccess = (taskId, fileUrls) => { 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) { if (index !== -1) {
tempList.value[index].status = 'success' tempList.value[index].status = 'success'
tempList.value[index].files = Array.isArray(fileUrls) ? fileUrls : [fileUrls] tempList.value[index].files = Array.isArray(fileUrls) ? fileUrls : [fileUrls]
} }
} }
const initHistoryList = (historyList) => { const initHistoryList = (historyList) => {
tempList.value = historyList tempList.value = historyList
currentPage.value = 1 currentPage.value = 1
hasMoreData.value = true hasMoreData.value = true
} }
const prependHistoryList = (historyList) => { const prependHistoryList = (historyList) => {
tempList.value = [...historyList, ...tempList.value] tempList.value = [...historyList, ...tempList.value]
} }
const appendHistoryList = (historyList) => { const appendHistoryList = (historyList) => {
tempList.value = [...tempList.value, ...historyList] tempList.value = [...tempList.value, ...historyList]
} }
const resetPagination = () => { const resetPagination = () => {
currentPage.value = 0 currentPage.value = 0
hasMoreData.value = true hasMoreData.value = true
@ -56,26 +56,26 @@ const DisplayStoreSetup = () => {
} }
const deleteHistoryItem = (id) => { const deleteHistoryItem = (id) => {
const index = tempList.value.findIndex(item => item.id === id) const index = tempList.value.findIndex((item) => item.id === id)
if (index !== -1) { if (index !== -1) {
tempList.value.splice(index, 1) tempList.value.splice(index, 1)
} }
} }
const scrollToBottom = async () => { const scrollToBottom = async () => {
const refValue = scrollerRef.value const refValue = scrollerRef.value
if (!refValue) { if (!refValue) {
return return
} }
try { try {
if (typeof refValue.scrollToBottom === 'function') { if (typeof refValue.scrollToBottom === 'function') {
await nextTick() await nextTick()
refValue.scrollToBottom() refValue.scrollToBottom()
return return
} }
const scrollerEl = refValue.$el const scrollerEl = refValue.$el
if (scrollerEl) { if (scrollerEl) {
const viewport = scrollerEl.querySelector('.vue-recycle-scroller__viewport') const viewport = scrollerEl.querySelector('.vue-recycle-scroller__viewport')
@ -83,7 +83,7 @@ const DisplayStoreSetup = () => {
viewport.scrollTop = viewport.scrollHeight viewport.scrollTop = viewport.scrollHeight
} }
} }
if (typeof refValue.scrollToItem === 'function' && tempList.value && tempList.value.length > 0) { if (typeof refValue.scrollToItem === 'function' && tempList.value && tempList.value.length > 0) {
await nextTick() await nextTick()
refValue.scrollToItem(tempList.value.length - 1) refValue.scrollToItem(tempList.value.length - 1)
@ -92,28 +92,28 @@ const DisplayStoreSetup = () => {
console.error('滚动出错:', error) console.error('滚动出错:', error)
} }
} }
const setResultData = (data) => { const setResultData = (data) => {
currentResultData.value = data currentResultData.value = data
} }
const setDialogBoxRef = (ref) => { const setDialogBoxRef = (ref) => {
dialogBoxRef.value = ref dialogBoxRef.value = ref
} }
const triggerGenerateWithResult = async () => { const triggerGenerateWithResult = async () => {
if (dialogBoxRef.value && currentResultData.value) { if (dialogBoxRef.value && currentResultData.value) {
await dialogBoxRef.value.fillParamsFromResult(currentResultData.value) await dialogBoxRef.value.fillParamsFromResult(currentResultData.value)
await dialogBoxRef.value.handleStart() await dialogBoxRef.value.handleStart()
} }
} }
const fillParamsForEdit = () => { const fillParamsForEdit = () => {
if (dialogBoxRef.value && currentResultData.value) { if (dialogBoxRef.value && currentResultData.value) {
dialogBoxRef.value.fillParamsFromResult(currentResultData.value) dialogBoxRef.value.fillParamsFromResult(currentResultData.value)
} }
} }
const openCanvas = (data) => { const openCanvas = (data) => {
if (typeof data === 'string') { if (typeof data === 'string') {
canvasImage.value = data canvasImage.value = data

View File

@ -1,5 +1,4 @@
const ParamStoreSetup = () => { const ParamStoreSetup = () => {
return { return {
} }
} }

View File

@ -7,7 +7,7 @@ export async function generateFilename(url, prefix = 'image') {
// 如果URL中没有文件名或扩展名根据类型生成 // 如果URL中没有文件名或扩展名根据类型生成
if (!filename || !filename.includes('.')) { if (!filename || !filename.includes('.')) {
const timestamp = new Date().getTime() const timestamp = Date.now()
// 根据URL内容推断文件类型否则默认为png // 根据URL内容推断文件类型否则默认为png
const extension = url.includes('.jpg') || url.includes('.jpeg') const extension = url.includes('.jpg') || url.includes('.jpeg')
? '.jpg' ? '.jpg'
@ -21,7 +21,7 @@ export async function generateFilename(url, prefix = 'image') {
} catch (error) { } catch (error) {
console.error('URL解析失败:', error) console.error('URL解析失败:', error)
// 如果URL解析失败生成默认文件名 // 如果URL解析失败生成默认文件名
const timestamp = new Date().getTime() const timestamp = Date.now()
return `${prefix}_${timestamp}.png` return `${prefix}_${timestamp}.png`
} }
} }

View File

@ -1,21 +1,20 @@
<template> <template>
<div style="width: 100%;display: flex;justify-content: center;align-items: center;transform: rotate(180deg);"> <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="primary-box" :class="{ 'none-primary-box': props.item.status === 'none' }">
<div ref="promptContainerRef" class="prompt-container">
<div class="prompt-container" ref="promptContainerRef"> <div ref="promptWrapperRef" class="prompt-wrapper">
<div class="prompt-wrapper" ref="promptWrapperRef"> <div ref="promptRef" class="prompt" :class="{ expanded: isHovering }" @mouseenter="isHovering = true" @mouseleave="isHovering = false">
<div class="prompt" ref="promptRef" :class="{ 'expanded': isHovering }" @mouseenter="isHovering = true" @mouseleave="isHovering = false">
<span class="prompt-text"> <span class="prompt-text">
{{ props.item.generateData.prompt || '生成图片' }} {{ props.item.generateData.prompt || '生成图片' }}
<i-ep-DocumentCopy class="Copy" @click.stop="copyPrompt"/> <i-ep-DocumentCopy class="Copy" @click.stop="copyPrompt" />
</span> </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 first-detailed-data">{{ props.item.generateData.model }}</div>
<div class="detailed-data">{{ props.item.generateData.proportion }}</div> <div class="detailed-data">{{ props.item.generateData.proportion }}</div>
</div> </div>
</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 first-detailed-data">{{ props.item.generateData.model }}</div>
<div class="detailed-data">{{ props.item.generateData.proportion }}</div> <div class="detailed-data">{{ props.item.generateData.proportion }}</div>
</div> </div>
@ -49,7 +48,7 @@
<!-- 已完成 图片 --> <!-- 已完成 图片 -->
<div v-if="props.item.status === 'success' && props.item.type === 'Painting'" class="box success-box"> <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" /> -->
<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" /> <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 class="left-top-btn collect-btn" @click="addCollection(file)"><img :src="isCollected(file) ? collectionActiveIcon : collectionIcon" /></div>
</div> </div>
<el-tooltip <el-tooltip
effect="dark" effect="dark"
content="画笔" content="画笔"
placement="top" placement="top"
:hide-after="0" :hide-after="0"
> >
<div @click.stop="AIbrush(file, index)" class="bottom-brush"> <div class="bottom-brush" @click.stop="AIbrush(file, index)">
<img :src="brush" /> <img :src="brush" />
</div> </div>
</el-tooltip> </el-tooltip>
@ -74,7 +73,7 @@
<!-- 已完成 视频 --> <!-- 已完成 视频 -->
<div v-if="props.item.status === 'success' && props.item.type === 'Video'" class="box success-box"> <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" /> --> <!-- <img :src="file" alt="index" class="img" /> -->
<video :src="props.item.files[0]" class="video" controls playsinline /> <video :src="props.item.files[0]" class="video" controls playsinline />
@ -97,16 +96,16 @@
</template> </template>
<script setup> <script setup>
import { cancelOrCollect, deleteGenerateHistory } from '@/apis/display'
import againGenerateIcon from '@/assets/display/againGenerate.svg'
import brush from '@/assets/display/brush.svg' import brush from '@/assets/display/brush.svg'
import collectionIcon from '@/assets/display/collection.svg'
import collectionActiveIcon from '@/assets/display/collection-active.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 { useDisplayStore, useParamStore, useUserStore } from '@/stores'
import { downloadImage } from '@/utils/downloadImage.js' 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({ const props = defineProps({
item: { item: {
@ -134,22 +133,22 @@ const checkTextOverflow = () => {
const lineHeight = 22.5 const lineHeight = 22.5
const padding = 8 const padding = 8
const twoLineHeight = lineHeight * 2 + padding const twoLineHeight = lineHeight * 2 + padding
promptRef.value.style.maxHeight = 'none' promptRef.value.style.maxHeight = 'none'
promptRef.value.style.overflow = 'hidden' promptRef.value.style.overflow = 'hidden'
const actualHeight = promptRef.value.scrollHeight const actualHeight = promptRef.value.scrollHeight
const lineCount = Math.ceil((actualHeight - padding) / lineHeight) const lineCount = Math.ceil((actualHeight - padding) / lineHeight)
if (!isHovering.value) { if (!isHovering.value) {
promptRef.value.style.maxHeight = `${twoLineHeight}px` promptRef.value.style.maxHeight = `${twoLineHeight}px`
promptRef.value.style.overflow = 'hidden' promptRef.value.style.overflow = 'hidden'
} }
showExternalGenerateData.value = lineCount >= 3 showExternalGenerateData.value = lineCount >= 3
if (!isHovering.value) { if (!isHovering.value) {
if(lineCount >= 3){ if (lineCount >= 3) {
promptContainerRef.value.style.height = `${twoLineHeight}px` promptContainerRef.value.style.height = `${twoLineHeight}px`
} else { } else {
promptContainerRef.value.style.height = `${actualHeight}px` promptContainerRef.value.style.height = `${actualHeight}px`
@ -163,8 +162,8 @@ watch(isHovering, (newVal) => {
if (promptRef.value) { if (promptRef.value) {
const lineHeight = 22.5 const lineHeight = 22.5
const padding = 8 const padding = 8
const twoLineHeight = lineHeight * 2 + padding const twoLineHeight = lineHeight * 2 + padding
if (newVal) { if (newVal) {
promptRef.value.style.maxHeight = 'none' promptRef.value.style.maxHeight = 'none'
promptRef.value.style.overflow = 'visible' promptRef.value.style.overflow = 'visible'
@ -208,7 +207,7 @@ const AIbrush = (file, index) => {
} }
const reEdit = () => { const reEdit = () => {
if(props.item.generateData?.modelType === 'edit'){ if (props.item.generateData?.modelType === 'edit') {
ElMessage.error('画笔生成的任务不能重新编辑') ElMessage.error('画笔生成的任务不能重新编辑')
return return
} }
@ -217,7 +216,7 @@ const reEdit = () => {
} }
const againGenerate = () => { const againGenerate = () => {
if(props.item.generateData?.modelType === 'edit'){ if (props.item.generateData?.modelType === 'edit') {
ElMessage.error('画笔生成的任务不能再次生成') ElMessage.error('画笔生成的任务不能再次生成')
return return
} }
@ -276,7 +275,7 @@ const addCollection = async (url) => {
const res = await cancelOrCollect({ const res = await cancelOrCollect({
taskId: props.item.id, taskId: props.item.id,
userId: useUser.userInfo.id, userId: useUser.userInfo.id,
url: url, url
}) })
if (res.success) { if (res.success) {
ElMessage.success(res.message || '操作成功') ElMessage.success(res.message || '操作成功')
@ -342,7 +341,7 @@ const copyPrompt = async () => {
width: 102%; width: 102%;
z-index: 5; z-index: 5;
width: auto; width: auto;
&.expanded{ &.expanded{
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
border-radius: 4px; border-radius: 4px;
@ -381,12 +380,12 @@ const copyPrompt = async () => {
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
line-height: normal; line-height: normal;
&.internal{ &.internal{
display: inline-flex; display: inline-flex;
margin-left: 10px; margin-left: 10px;
} }
&.external{ &.external{
position: absolute; position: absolute;
right: -16px; right: -16px;

View File

@ -34,7 +34,7 @@
</div> </div>
<span class="line"></span> <span class="line"></span>
<div class="btn"> <div class="btn">
<Select v-model="selectedFavorite" :options="favoriteOptions" width="auto" > <Select v-model="selectedFavorite" :options="favoriteOptions" width="auto">
<template #prefix> <template #prefix>
<i-ep-Star /> <i-ep-Star />
</template> </template>
@ -43,8 +43,8 @@
</div> </div>
<VirtualScroller <VirtualScroller
ref="scrollerRef"
v-if="props.if" v-if="props.if"
ref="scrollerRef"
:items="list" :items="list"
key-field="id" key-field="id"
:estimated-height="300" :estimated-height="300"
@ -66,17 +66,17 @@
</template> </template>
<script setup> <script setup>
import { useDisplayStore, useParamStore, useUserStore } from '@/stores'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import Set from './components/set.vue' import { useRouter } from 'vue-router'
import RefreshOverlay from './components/RefreshOverlay.vue' import { requestTaskHistory } from '@/apis/display'
import Canvas from '@/components/canvas/index.vue'
import Select from '@/components/Select/index.vue' import Select from '@/components/Select/index.vue'
import { VirtualScroller } from '@/components/virtual-scroller' import { VirtualScroller } from '@/components/virtual-scroller'
import Canvas from '@/components/canvas/index.vue' import { useDisplayStore, useParamStore, useUserStore } from '@/stores'
import { requestTaskHistory } from '@/apis/display'
import { useRouter } from 'vue-router'
import { getChargeType } from '@/utils/taskPolling'
import { getPlatformCode } from '@/utils/modelApi' 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({ const props = defineProps({
if: { if: {
@ -138,7 +138,7 @@ const toggleDisplay = (newValue, oldValue) => {
const conversion = (newlist) => { const conversion = (newlist) => {
const temp = newlist.map((item) => { const temp = newlist.map((item) => {
// outputs URL // outputs URL
const files = item.outputs?.map(o => o.url) || [] const files = item.outputs?.map((o) => o.url) || []
const request = item.request || {} const request = item.request || {}
const generateData = { const generateData = {
model: item.model_name || '', model: item.model_name || '',
@ -152,7 +152,7 @@ const conversion = (newlist) => {
customHight: request.customHight, customHight: request.customHight,
duration: request.duration || '', duration: request.duration || '',
videoPattern: request.videoPattern || '', videoPattern: request.videoPattern || '',
modelParams: { ...request }, modelParams: { ...request }
} }
// API status UI // API status UI
let uiStatus = 'success' let uiStatus = 'success'
@ -168,7 +168,7 @@ const conversion = (newlist) => {
generateData, generateData,
time: item.created_at || '', time: item.created_at || '',
files, files,
collectStatus: item.collectStatus || {}, collectStatus: item.collectStatus || {}
} }
}) })
return temp return temp
@ -235,7 +235,6 @@ const fetchHistory = async (isLoadMore = false) => {
} }
hasMoreData.value = dataList.length === 10 hasMoreData.value = dataList.length === 10
} catch (error) { } catch (error) {
console.error('获取历史失败:', error) console.error('获取历史失败:', error)
ElMessage({ ElMessage({
@ -380,5 +379,4 @@ onBeforeUnmount(() => {
background-color: #ccc; background-color: #ccc;
} }
} }
</style> </style>

View File

@ -1,8 +1,8 @@
<script setup> <script setup>
import { computed, ref, onMounted, watch } from 'vue' import { computed, onMounted, ref, watch } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import display from './display/index.vue'
import { useDisplayStore } from '@/stores' import { useDisplayStore } from '@/stores'
import display from './display/index.vue'
const route = useRoute() const route = useRoute()
const useDisplay = useDisplayStore() const useDisplay = useDisplayStore()

View File

@ -4,14 +4,14 @@
<div class="icon-wrapper"> <div class="icon-wrapper">
<div class="lock-icon"> <div class="lock-icon">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <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> </svg>
</div> </div>
</div> </div>
<h1 class="title">账号未登录</h1> <h1 class="title">账号未登录</h1>
<p class="description">请先登录以访问完整功能</p> <p class="description">请先登录以访问完整功能</p>
<div class="action-buttons"> <div class="action-buttons">
<button class="btn-primary" @click="handleLogin"> <button class="btn-primary" @click="handleLogin">
立即登录 立即登录
@ -20,7 +20,7 @@
注册账号 注册账号
</button> </button>
</div> </div>
<div class="features"> <div class="features">
<div class="feature-item"> <div class="feature-item">
<div class="feature-icon"></div> <div class="feature-icon"></div>
@ -36,7 +36,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="background-decoration"> <div class="background-decoration">
<div class="circle circle-1"></div> <div class="circle circle-1"></div>
<div class="circle circle-2"></div> <div class="circle circle-2"></div>
@ -311,15 +311,15 @@ const handleRegister = () => {
.login-content { .login-content {
padding: 40px 20px; padding: 40px 20px;
} }
.title { .title {
font-size: 24px; font-size: 24px;
} }
.action-buttons { .action-buttons {
flex-direction: column; flex-direction: column;
} }
.features { .features {
flex-direction: column; flex-direction: column;
gap: 15px; gap: 15px;