新逻辑

This commit is contained in:
王佑琳 2026-03-10 18:09:37 +08:00
parent e6290b53e5
commit c9ed299ef6
14 changed files with 1011 additions and 662 deletions

View File

@ -11,8 +11,8 @@ VITE_API_PAY_PREFIX = '/pay'
VITE_API_PAY_TARGET = 'http://test.xueai.art' # http://43.248.133.202 test.xueai.art
# 任务处理模块
VITE_API_WORKFLOW_UPLOAD = 'http://aipaint.xueai.art/aigc/workflow/file/upload' # https://sxwz.xueai.art/workflow https://designtools.xueai.art/workflow
VITE_API_WORKFLOW_WS = 'wss://aipaint.xueai.art/testworkflow'
VITE_API_WORKFLOW_UPLOAD = 'http://43.248.97.19:4000/aigc/workflow/file/upload' # https://sxwz.xueai.art/workflow https://designtools.xueai.art/workflow
VITE_API_WORKFLOW_WS = 'ws://43.248.97.19:4000/testworkflow'
# 是否开启开发者工具
VITE_OPEN_DEVTOOLS = false

2
components.d.ts vendored
View File

@ -61,7 +61,9 @@ declare module 'vue' {
IEpStarFilled: typeof import('~icons/ep/star-filled')['default']
IEpTop: typeof import('~icons/ep/top')['default']
IEpUploadFilled: typeof import('~icons/ep/upload-filled')['default']
ImageUploader: typeof import('./src/components/dialogBox/imageUploader/index.vue')['default']
Img: typeof import('./src/components/Img/index.vue')['default']
'Index(1)': typeof import('./src/components/canvas/index(1).vue')['default']
Library: typeof import('./src/components/Library/index.vue')['default']
Model: typeof import('./src/components/dialogBox/model/index.vue')['default']
ModelParam: typeof import('./src/components/AIgenerate/components/modelParam.vue')['default']

View File

@ -0,0 +1,4 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2L7 7L2 2" stroke="#666666" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 7L7 12L2 7" stroke="#666666" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 284 B

View File

@ -15,24 +15,11 @@
>
<div class="uploader-icon"><i-ep-plus /></div>
</el-upload>
<!-- 模特库组件 -->
<div>
<Library v-if="useDisplay[props.type].Library" :type="props.type" @select="handleSelect">
<template #filters>
<slot name="filters" />
</template>
<template #create-settings>
<slot name="create-settings" />
</template>
</Library>
<collection v-if="useDisplay[props.type].collection" :type="props.type" @select="handleSelect" />
</div>
</template>
<script setup>
import { genFileId } from 'element-plus'
import Library from '@/components/Library/index.vue'
import { useDisplayStore, useParamStore } from '@/stores'
import { useDisplayStore } from '@/stores'
const props = defineProps({
type: {
@ -50,7 +37,6 @@ const imageNum = ref(0)
const images = ref([])
const useParams = useParamStore()
const uploadRef = ref(null)
const useDisplay = useDisplayStore()
const handleSelect = async (url) => {
// URLBlob

View File

@ -1,14 +1,16 @@
<template>
<Teleport to="body">
<div v-if="visible" class="canvas-modal">
<!-- 主弹窗 -->
<div class="canvas-container">
<div class="header">
<div class="preview-area">
<span class="preview-text">图片预览</span>
</div>
<div class="close-btn" @click="handleClose">
<svg viewBox="0 0 1024 1024" width="24" height="24">
<path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm165.4 618.2l-66 0.3L512 563.4l-99.3 119-66.1-0.3c-4.4 0-8-3.6-8-8 0-1.9 0.7-3.7 1.9-5.2l130.1-155L340.5 359a8.32 8.32 0 0 1-1.9-5.2c0-4.4 3.6-8 8-8l66.1 0.3L512 464.6l99.3-118.4 66-0.3c4.4 0 8 3.6 8 8 0 1.9-0.7 3.7-1.9 5.2L553.5 514l130 155c1.2 1.5 1.9 3.3 1.9 5.2 0 4.4-3.6 8-8 8z" fill="#333"/>
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="13" viewBox="0 0 12 13" fill="none">
<path d="M1.28809 1.01469L10.9873 11.0052" stroke="#666666" stroke-width="2" stroke-linecap="round"/>
<path d="M10.6846 1.02413L0.985354 11.0147" stroke="#666666" stroke-width="2" stroke-linecap="round"/>
</svg>
</div>
</div>
@ -25,74 +27,137 @@
<div class="footer">
<div class="input-area">
<el-input
v-model="inputText"
type="textarea"
:rows="3"
placeholder="请输入描述..."
resize="none"
/>
<div class="custom-input">
<div
ref="editableDivRef"
contenteditable="true"
class="custom-textarea"
@input="handleInput"
:data-placeholder="!inputText ? '请输入提示词或使用圆形/矩形工具' : ''"
></div>
</div>
</div>
<div class="control-area">
<div class="shape-selector">
<div
class="shape-btn"
:class="{ active: currentShape === 'rectangle' }"
@click="currentShape = 'rectangle'"
>
<svg viewBox="0 0 1024 1024" width="20" height="20">
<path d="M880 112H144c-17.7 0-32 14.3-32 32v736c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V144c0-17.7-14.3-32-32-32z m-40 728H184V184h656v656z" fill="#333"/>
</svg>
<span>矩形</span>
<div class="shape-selector-container">
<div class="shape-selector">
<!-- 矩形 -->
<div
class="shape-btn"
:class="{ active: currentShape === 'rectangle' }"
@click="currentShape = 'rectangle'"
>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none">
<rect x="1.84961" y="1.84998" width="14.3" height="14.3" rx="1.5" :stroke="currentShape === 'rectangle' ? '#000F33' : '#888888'"/>
</svg>
</div>
<!-- 圆形 -->
<div
class="shape-btn"
:class="{ active: currentShape === 'circle' }"
@click="currentShape = 'circle'"
>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none">
<circle cx="9.00039" cy="9.00002" r="7.6" :stroke="currentShape === 'circle' ? '#000F33' : '#888888'"/>
</svg>
</div>
</div>
<div
class="shape-btn"
:class="{ active: currentShape === 'circle' }"
@click="currentShape = 'circle'"
>
<svg viewBox="0 0 1024 1024" width="20" height="20">
<path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64z m0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z" fill="#333"/>
</svg>
<span>圆形</span>
<div class="action-btns">
<!-- 上一步 -->
<div class="shape-btn" @click="undo">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none">
<path d="M1.64645 3.64645C1.45118 3.84171 1.45118 4.15829 1.64645 4.35355L4.82843 7.53553C5.02369 7.7308 5.34027 7.7308 5.53553 7.53553C5.7308 7.34027 5.7308 7.02369 5.53553 6.82843L2.70711 4L5.53553 1.17157C5.7308 0.976311 5.7308 0.659728 5.53553 0.464466C5.34027 0.269204 5.02369 0.269204 4.82843 0.464466L1.64645 3.64645ZM2 14.5C1.72386 14.5 1.5 14.7239 1.5 15C1.5 15.2761 1.72386 15.5 2 15.5V15V14.5ZM2 4V4.5H10.5V4V3.5H2V4ZM10.5 15V14.5H2V15V15.5H10.5V15ZM16 9.5H15.5C15.5 12.2614 13.2614 14.5 10.5 14.5V15V15.5C13.8137 15.5 16.5 12.8137 16.5 9.5H16ZM16 9.5H16.5C16.5 6.18629 13.8137 3.5 10.5 3.5V4V4.5C13.2614 4.5 15.5 6.73858 15.5 9.5H16Z" fill="#000F33"/>
</svg>
</div>
<!-- 下一步 -->
<div class="shape-btn" @click="redo">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none">
<path d="M16.3536 3.64645C16.5488 3.84171 16.5488 4.15829 16.3536 4.35355L13.1716 7.53553C12.9763 7.7308 12.6597 7.7308 12.4645 7.53553C12.2692 7.34027 12.2692 7.02369 12.4645 6.82843L15.2929 4L12.4645 1.17157C12.2692 0.976311 12.2692 0.659728 12.4645 0.464466C12.6597 0.269204 12.9763 0.269204 13.1716 0.464466L16.3536 3.64645ZM16 13.5C16.2761 13.5 16.5 13.7239 16.5 14C16.5 14.2761 16.2761 14.5 16 14.5V14V13.5ZM16 4V4.5H8V4V3.5H16V4ZM8 14V13.5H16V14V14.5H8V14ZM3 9H3.5C3.5 11.4853 5.51472 13.5 8 13.5V14V14.5C4.96243 14.5 2.5 12.0376 2.5 9H3ZM3 9H2.5C2.5 5.96243 4.96243 3.5 8 3.5V4V4.5C5.51472 4.5 3.5 6.51472 3.5 9H3Z" fill="#000F33"/>
</svg>
</div>
<!-- 删除 -->
<div class="shape-btn" @click="deleteShape">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none">
<path d="M1.7998 3.60002H4.4998M16.1998 3.60002H13.4998M13.4998 3.60002V2.90003C13.4998 1.79546 12.6044 0.900024 11.4998 0.900024H6.4998C5.39523 0.900024 4.4998 1.79545 4.4998 2.90002V3.60002M13.4998 3.60002H4.4998" stroke="#000F33" stroke-linecap="round"/>
<path d="M3.59961 6.29999V14.2C3.59961 15.3046 4.49504 16.2 5.59961 16.2H12.3996C13.5042 16.2 14.3996 15.3046 14.3996 14.2V6.29999M7.19961 7.19999V14.4M10.7996 7.19999V14.4" stroke="#000F33" stroke-linecap="round"/>
</svg>
</div>
</div>
</div>
<div class="action-btns">
<div class="shape-btn" @click="undo">
<svg viewBox="0 0 1024 1024" width="20" height="20">
<path d="M384 512c0-70.7 57.3-128 128-128s128 57.3 128 128v192h-64v-192c0-35.3-28.7-64-64-64s-64 28.7-64 64v192h-64v-192z" fill="#333"/>
<path d="M704 256H192c-35.3 0-64 28.7-64 64v256c0 35.3 28.7 64 64 64h512c35.3 0 64-28.7 64-64V320c0-35.3-28.7-64-64-64z m0 320H192V320h512v256z" fill="#333"/>
</svg>
<span>上一步</span>
</div>
<div class="shape-btn" @click="redo">
<svg viewBox="0 0 1024 1024" width="20" height="20">
<path d="M640 512c0 70.7-57.3 128-128 128s-128-57.3-128-128v-192h64v192c0 35.3 28.7 64 64 64s64-28.7 64-64v-192h64v192z" fill="#333"/>
<path d="M320 256H832c35.3 0 64 28.7 64 64v256c0 35.3-28.7 64-64 64H320c-35.3 0-64-28.7-64-64V320c0-35.3 28.7-64 64-64z m0 320V320h512v256H320z" fill="#333"/>
</svg>
<span>下一步</span>
</div>
<div class="shape-btn" @click="deleteShape">
<svg viewBox="0 0 1024 1024" width="20" height="20">
<path d="M416 256h192v512H416V256z m128 640h-64v-448h64v448z m-192 0h-64v-448h64v448z m384-640v512h-192V256h192z m-128 640h-64v-448h64v448z" fill="#333"/>
<path d="M864 192H640v-64c0-35.2-28.8-64-64-64H448c-35.2 0-64 28.8-64 64v64H160c-35.2 0-64 28.8-64 64v32h768v-32c0-35.2-28.8-64-64-64z" fill="#333"/>
</svg>
<span>删除</span>
</div>
</div>
<el-button type="primary" @click="handleSend">发送</el-button>
<button class="confirm-btn" @click="handleSend">发送</button>
</div>
</div>
</div>
<!-- 右侧弹窗 -->
<div v-if="brushPanelVisible" class="brush-panel">
<div class="brush-panel-row first-row">
<span class="brush-panel-title">请输入替换内容描述</span>
<div class="brush-panel-close" @click="closeBrushPanel">
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="13" viewBox="0 0 12 13" fill="none">
<path d="M1.28809 1.01468L10.9873 11.0052" stroke="#666666" stroke-width="2" stroke-linecap="round"/>
<path d="M10.6846 1.02411L0.985354 11.0147" stroke="#666666" stroke-width="2" stroke-linecap="round"/>
</svg>
</div>
</div>
<div class="brush-panel-row second-row">
<div
ref="brushTextareaRef"
contenteditable="true"
class="brush-textarea"
@input="handleBrushInput"
:data-placeholder="!currentShapeDescription ? '请输入描述...' : ''"
></div>
</div>
<div class="brush-panel-row third-row">
<span class="brush-panel-text">请选择参考图进行替换</span>
</div>
<div class="brush-panel-row fourth-row">
<div class="reference-images">
<div
v-for="(img, index) in allReferenceImages"
:key="index"
class="reference-image-item"
:class="{ selected: selectedReferenceImages.includes(img) }"
@click="selectReferenceImage(index)"
>
<img :src="img" alt="参考图" />
<div class="reference-image-delete" @click.stop="removeReferenceImage(index)">
<svg xmlns="http://www.w3.org/2000/svg" width="7" height="7" viewBox="0 0 7 7" fill="none">
<path d="M0.5 0.5L6.5 6.5M6.5 0.5L0.5 6.5" stroke="white" stroke-linecap="round"/>
</svg>
</div>
</div>
<div
v-if="allReferenceImages.length < 5"
class="reference-image-upload"
@click="handleUploadReference"
>
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 15 15" fill="none">
<path d="M7.49316 0C7.76931 3.42279e-07 7.99316 0.223857 7.99316 0.5V6.99316H14.5C14.7761 6.99316 15 7.21702 15 7.49316C14.9999 7.76925 14.7761 7.99316 14.5 7.99316H7.99316V14.5C7.99316 14.7761 7.76931 15 7.49316 15C7.21702 15 6.99316 14.7761 6.99316 14.5V7.99316H0.5C0.223898 7.99316 6.59601e-05 7.76925 0 7.49316C0 7.21702 0.223858 6.99316 0.5 6.99316H6.99316V0.5C6.99316 0.223857 7.21702 1.20706e-08 7.49316 0Z" fill="#000F33"/>
</svg>
</div>
</div>
</div>
<div class="brush-panel-row fifth-row">
<button class="confirm-btn" @click="handleBrushConfirm">确定</button>
</div>
</div>
</div>
</Teleport>
</template>
<script setup>
import { ref, watch, nextTick } from 'vue'
import { ref, watch, nextTick, computed } from 'vue'
const props = defineProps({
visible: {
@ -108,6 +173,8 @@ const props = defineProps({
const emit = defineEmits(['update:visible', 'send'])
const canvasRef = ref(null)
const editableDivRef = ref(null)
const brushTextareaRef = ref(null)
const currentImage = ref('')
const bgImage = ref(null)
const scale = ref(1)
@ -119,8 +186,56 @@ const startY = ref(0)
const shapes = ref([])
const history = ref([])
const historyIndex = ref(-1)
const promptHistory = ref([])
const promptHistoryIndex = ref(-1)
const allReferenceImages = ref([])
const shapeColors = ['#ff0000', '#ff7f00', '#00ff00', '#0000ff', '#8b00ff']
const maxShapes = 5
const brushPanelVisible = ref(false)
const currentShapeDescription = ref('')
const selectedReferenceImages = ref([])
const currentEditingShapeIndex = ref(-1)
const isPanelOpen = ref(false)
const currentEditingContent = ref('')
const handleInput = (e) => {
inputText.value = e.target.innerHTML
}
const handleBrushInput = (e) => {
currentShapeDescription.value = e.target.innerText
}
watch(currentShapeDescription, (newVal) => {
updateCurrentEditingContent(newVal)
})
watch(selectedReferenceImages, (newVal) => {
updateCurrentEditingContent(currentShapeDescription.value)
})
const updateCurrentEditingContent = (description) => {
const colorNames = ['红色', '橙色', '绿色', '蓝色', '紫色']
const colorIndex = currentEditingShapeIndex.value >= 0 ? currentEditingShapeIndex.value : 0
const colorName = colorNames[colorIndex % colorNames.length]
const isFirstShape = shapes.value.length === 0 || (currentEditingShapeIndex.value === 0 && shapes.value.length === 1)
const currentShape = currentEditingShapeIndex.value >= 0 ? shapes.value[currentEditingShapeIndex.value] : null
const shapeType = currentShape ? currentShape.type : 'rectangle'
const shapeWord = shapeType === 'circle' ? '圈' : '框'
if (selectedReferenceImages.value.length > 0) {
const imageIndex = allReferenceImages.value.indexOf(selectedReferenceImages.value[0]) + 1
const prefix = isFirstShape ? '' : ''
currentEditingContent.value = `${prefix}${colorName}${shapeWord}内的【XXX】替换为【图${imageIndex}中的${description}`
} else if (description) {
const prefix = isFirstShape ? '' : ''
currentEditingContent.value = `${prefix}${colorName}${shapeWord}内的【XXX】替换为【${description}`
} else {
const prefix = isFirstShape ? '' : ''
currentEditingContent.value = `${prefix}${colorName}${shapeWord}内的【XXX】替换为【XXX】或【图X中的XXX】`
}
}
watch(() => props.visible, (newVal) => {
if (newVal) {
@ -128,6 +243,9 @@ watch(() => props.visible, (newVal) => {
bgImage.value = null
history.value = []
historyIndex.value = -1
promptHistory.value = []
promptHistoryIndex.value = -1
allReferenceImages.value = []
nextTick(() => {
initCanvas()
})
@ -193,6 +311,10 @@ const initCanvas = () => {
}
const handleMouseDown = (e) => {
if (isPanelOpen.value) {
ElMessage.warning('请先完成当前图形的编辑')
return
}
if (shapes.value.length >= maxShapes) {
ElMessage.warning('最多只能画5笔')
return
@ -270,31 +392,48 @@ const handleMouseUp = (e) => {
}
const colorIndex = shapes.value.length
shapes.value.push({
const newShape = {
type: currentShape.value,
startX: startX.value,
startY: startY.value,
endX: endX,
endY: endY,
color: shapeColors[colorIndex]
})
color: shapeColors[colorIndex],
description: '',
referenceImages: []
}
shapes.value.push(newShape)
currentEditingShapeIndex.value = shapes.value.length - 1
saveHistory()
isDrawing.value = false
brushPanelVisible.value = true
isPanelOpen.value = true
currentShapeDescription.value = ''
selectedReferenceImages.value = []
}
const saveHistory = () => {
history.value = history.value.slice(0, historyIndex.value + 1)
history.value.push([...shapes.value])
historyIndex.value = history.value.length - 1
promptHistory.value = promptHistory.value.slice(0, promptHistoryIndex.value + 1)
promptHistory.value.push(inputText.value)
promptHistoryIndex.value = promptHistory.value.length - 1
}
const undo = () => {
if (historyIndex.value >= 0) {
const newIndex = historyIndex.value - 1
historyIndex.value = newIndex
shapes.value = newIndex >= 0 ? [...history.value[newIndex]] : []
historyIndex.value--
shapes.value = historyIndex.value >= 0 ? [...history.value[historyIndex.value]] : []
redrawCanvas()
promptHistoryIndex.value--
inputText.value = promptHistoryIndex.value >= 0 ? promptHistory.value[promptHistoryIndex.value] : ''
if (editableDivRef.value) {
editableDivRef.value.innerHTML = inputText.value
}
}
}
@ -303,12 +442,27 @@ const redo = () => {
historyIndex.value++
shapes.value = [...history.value[historyIndex.value]]
redrawCanvas()
promptHistoryIndex.value++
inputText.value = promptHistory.value[promptHistoryIndex.value]
if (editableDivRef.value) {
editableDivRef.value.innerHTML = inputText.value
}
}
}
const deleteShape = () => {
shapes.value = []
saveHistory()
inputText.value = ''
promptHistory.value = []
promptHistoryIndex.value = -1
history.value = []
historyIndex.value = -1
if (editableDivRef.value) {
editableDivRef.value.innerHTML = ''
}
redrawCanvas()
}
@ -353,6 +507,8 @@ const handleClose = () => {
emit('update:visible', false)
shapes.value = []
inputText.value = ''
promptHistory.value = []
promptHistoryIndex.value = -1
}
const handleSend = () => {
@ -365,6 +521,87 @@ const handleSend = () => {
})
handleClose()
}
const removeReferenceImage = (index) => {
allReferenceImages.value.splice(index, 1)
}
const selectReferenceImage = (index) => {
const img = allReferenceImages.value[index]
const existingIndex = selectedReferenceImages.value.indexOf(img)
if (existingIndex !== -1) {
selectedReferenceImages.value = []
} else {
selectedReferenceImages.value = [img]
}
}
const handleUploadReference = () => {
const input = document.createElement('input')
input.type = 'file'
input.accept = 'image/*'
input.multiple = true
input.onchange = (e) => {
const files = Array.from(e.target.files)
const remainingSlots = 5 - allReferenceImages.value.length
if (remainingSlots <= 0) {
ElMessage.warning('最多只能上传5张图片')
return
}
const filesToUpload = files.slice(0, remainingSlots)
filesToUpload.forEach(file => {
const reader = new FileReader()
reader.onload = (event) => {
allReferenceImages.value.push(event.target.result)
}
reader.readAsDataURL(file)
})
}
input.click()
}
const closeBrushPanel = () => {
if (currentEditingShapeIndex.value >= 0) {
shapes.value.splice(currentEditingShapeIndex.value, 1)
redrawCanvas()
}
brushPanelVisible.value = false
isPanelOpen.value = false
currentShapeDescription.value = ''
selectedReferenceImages.value = []
currentEditingShapeIndex.value = -1
currentEditingContent.value = ''
}
const handleBrushConfirm = () => {
if (currentEditingShapeIndex.value >= 0) {
shapes.value[currentEditingShapeIndex.value].description = currentShapeDescription.value
shapes.value[currentEditingShapeIndex.value].referenceImages = [...selectedReferenceImages.value]
}
if (currentEditingContent.value) {
if (editableDivRef.value) {
const currentContent = editableDivRef.value.innerHTML
editableDivRef.value.innerHTML = currentContent + currentEditingContent.value
inputText.value = editableDivRef.value.innerHTML
} else {
inputText.value += currentEditingContent.value
}
}
saveHistory()
brushPanelVisible.value = false
isPanelOpen.value = false
currentShapeDescription.value = ''
selectedReferenceImages.value = []
currentEditingContent.value = ''
}
</script>
<style lang="less" scoped>
@ -382,28 +619,33 @@ const handleSend = () => {
}
.canvas-container {
width: 40vw;
height: 70vh;
width: 37vw;
height: 89vh;
min-width: 440px;
min-height: 600px;
display: flex;
flex-direction: column;
background: #f5f5f5;
background: #fff;
border-radius: 20px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 20px;
padding: 10px 30px 0 30px;
background: #fff;
border-bottom: 1px solid #e4e7ed;
border-radius: 20px;
.preview-area {
.preview-text {
color: #333;
font-family: "Microsoft YaHei";
font-size: 14px;
font-weight: 400;
}
font-size: 16px;
font-style: normal;
font-weight: 700;
line-height: normal;
}
}
.close-btn {
@ -423,14 +665,16 @@ const handleSend = () => {
}
.canvas-area {
width: 100%;
// width: 100%;
height: 100%;
flex: 1;
overflow: auto;
display: flex;
align-items: center;
justify-content: center;
background: #f3f4f5;
background: #F8F9FA;
margin: 20px 30px;
border-radius: 10px;
canvas {
max-width: 100%;
@ -441,11 +685,56 @@ const handleSend = () => {
.footer {
background: #fff;
border-top: 1px solid #e4e7ed;
padding: 15px 20px;
padding: 0 30px 30px 30px;
border-radius: 20px;
.input-area {
margin-bottom: 15px;
.custom-input {
width: 100%;
background: #F8F9FA;
border-radius: 10px;
padding: 10px 10px 10px 20px;
transition: border-color 0.3s;
display: flex;
align-items: flex-start;
gap: 8px;
&:focus-within {
border-color: #409eff;
}
.custom-textarea {
flex: 1;
border: none;
outline: none;
resize: none;
font-family: inherit;
font-size: 15px;
line-height: 1.5;
color: #333;
min-height: 20px;
max-height: 67px;
overflow-y: auto;
&:empty::before {
content: attr(data-placeholder);
color: #999;
pointer-events: none;
}
:deep(.template-image) {
width: 20px;
height: 20px;
object-fit: cover;
border-radius: 4px;
border: 2px solid #409eff;
vertical-align: middle;
margin: 0 2px;
}
}
}
}
.control-area {
@ -454,28 +743,37 @@ const handleSend = () => {
align-items: center;
}
.shape-selector-container {
display: flex;
align-items: center;
gap: 0px;
height: 32px;
width: 230px;
padding: 0 20px;
background: #F8F9FA;
border-radius: 10px;
}
.shape-selector {
display: flex;
gap: 10px;
gap: 20px;
padding-right: 20px;
border-right: 1px solid rgba(0, 0, 0, 0.10);
.shape-btn {
display: flex;
align-items: center;
gap: 5px;
padding: 8px 16px;
border: 1px solid #dcdfe6;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s;
&:hover {
border-color: #409eff;
color: #409eff;
// border-color: #409eff;
// color: #409eff;
}
&.active {
background: #409eff;
border-color: #409eff;
color: #fff;
svg {
@ -487,22 +785,252 @@ const handleSend = () => {
.action-btns {
display: flex;
gap: 10px;
margin-left: 20px;
gap: 20px;
padding-left: 20px;
.shape-btn {
display: flex;
align-items: center;
gap: 5px;
padding: 8px 16px;
border: 1px solid #dcdfe6;
// border: 1px solid #dcdfe6;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s;
&:hover {
border-color: #409eff;
color: #409eff;
// border-color: #409eff;
// color: #409eff;
}
}
}
}
.confirm-btn {
width: 100px;
height: 32px;
border: none;
background: #000F33;
border-radius: 10px;
color: #FFF;
font-family: "Microsoft YaHei";
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: normal;
}
.brush-panel {
position: absolute;
right: 5vw;
top: 50%;
transform: translateY(-50%);
width: 390px;
background: #fff;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 10000;
border-radius: 20px;
.brush-panel-row {
// padding: 15px 20px;
&.first-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
.brush-panel-title {
color: #333;
font-family: "Microsoft YaHei";
font-size: 16px;
font-style: normal;
font-weight: 700;
line-height: normal;
}
.brush-panel-close {
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 4px;
transition: background 0.3s;
&:hover {
background: #f5f5f5;
}
}
}
&.second-row {
padding: 0px 20px;
.brush-textarea {
width: 100%;
font-size: 14px;
line-height: 1.5;
color: #333;
outline: none;
overflow-y: auto;
scrollbar-width: none;
-ms-overflow-style: none;
background: #F8F9FA;
border-radius: 10px;
padding: 10px 10px 10px 20px;
transition: border-color 0.3s;
display: flex;
align-items: flex-start;
gap: 8px;
min-height: 20px;
max-height: 59px;
&::-webkit-scrollbar {
display: none;
}
&:focus {
border-color: #409eff;
}
&:empty::before {
content: attr(data-placeholder);
color: #999;
pointer-events: none;
}
}
}
&.third-row {
padding: 30px 20px 0px 20px;
.brush-panel-text {
color: #333;
font-family: "Microsoft YaHei";
font-size: 16px;
font-style: normal;
font-weight: 700;
line-height: normal;
}
}
&.fourth-row {
padding: 20px 20px 30px 20px;
.reference-images {
display: flex;
flex-wrap: wrap;
gap: 10px;
.reference-image-item {
position: relative;
width: 60px;
height: 60px;
border-radius: 4px;
border: 1px solid #FFF;
cursor: pointer;
transition: all 0.3s;
&.selected {
border-color: #000F33;
}
img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 4px;
}
.reference-image-delete {
position: absolute;
top: -8px;
right: -8px;
width: 16px;
height: 16px;
background: rgba(0, 0, 0, 0.6);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: background 0.3s, opacity 0.3s;
opacity: 0;
&:hover {
background: rgba(0, 0, 0, 0.8);
}
}
&:hover .reference-image-delete {
opacity: 1;
}
}
.reference-image-upload {
width: 60px;
height: 60px;
background: #F8F9FA;
border-radius: 4px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s;
border-radius: 10px;
&:hover {
border-color: #409eff;
color: #409eff;
svg {
fill: #409eff;
}
}
span {
font-size: 12px;
color: #999;
margin-top: 4px;
}
}
}
}
&.fifth-row {
display: flex;
justify-content: flex-end;
padding: 0px 20px 30px 20px;
.confirm-btn {
padding: 8px 24px;
color: #fff;
border: none;
font-size: 14px;
cursor: pointer;
transition: background 0.3s;
width: 100px;
height: 32px;
background: #000F33;
border-radius: 10px;
font-family: "Microsoft YaHei";
font-style: normal;
font-weight: 400;
line-height: normal;
display: flex;
align-items: center;
justify-content: center;
&:hover {
background: #66b1ff;
}
&:active {
background: #3a8ee6;
}
}
}
}

View File

@ -0,0 +1,231 @@
<template>
<div class="image-uploader">
<div class="image-list">
<div
v-for="(item, index) in imageList"
:key="item.uid"
class="image-item"
@click.stop
>
<img :src="item.url" class="uploaded-image" alt="上传的图片" />
<div class="image-index">{{ index + 1 }}</div>
<div class="delete-icon" @click="handleDelete(index)">
<i-ep-close />
</div>
</div>
<div v-if="imageList.length < limit" class="upload-trigger" @click="triggerUpload">
<i-ep-plus />
<span>参考内容</span>
</div>
</div>
<el-upload
ref="uploadRef"
v-show="false"
:action="uploadurl"
multiple
:limit="limit"
:before-upload="beforeUpload"
:on-success="handleSuccess"
:on-error="handleError"
:on-exceed="handleExceed"
/>
</div>
</template>
<script setup>
import { genFileId } from 'element-plus'
const props = defineProps({
limit: {
type: Number,
default: 1
},
modelValue: {
type: Array,
default: () => []
}
})
const emit = defineEmits(['update:modelValue'])
const uploadurl = import.meta.env.VITE_API_WORKFLOW_UPLOAD
const uploadRef = ref(null)
const imageList = ref([...props.modelValue])
watch(() => props.modelValue, (newVal) => {
imageList.value = [...newVal]
}, { deep: true })
const triggerUpload = () => {
uploadRef.value.$el.querySelector('input').click()
}
const beforeUpload = (rawFile) => {
const allowedTypes = ['image/jpeg', 'image/png']
if (!allowedTypes.includes(rawFile.type)) {
ElMessage.error('不支持的文件类型,请上传 JPG、PNG 格式的图片')
return false
}
if (rawFile.size / 1024 / 1024 > 10) {
ElMessage.error('图片大小不能超过 10MB')
return false
}
}
const handleSuccess = (response, uploadFile) => {
ElMessage.success('上传成功')
const newImage = {
uid: uploadFile.uid,
url: response.url
}
imageList.value.push(newImage)
emit('update:modelValue', [...imageList.value])
}
const handleError = () => {
ElMessage.error('上传失败,请重新选择图片')
}
const handleExceed = () => {
ElMessage.warning(`最多只能上传 ${props.limit} 张图片`)
}
const handleDelete = (index) => {
imageList.value.splice(index, 1)
emit('update:modelValue', [...imageList.value])
}
const handleSelect = async (url) => {
const initialFile = await fetch(url)
const blob = await initialFile.blob()
const file = new File([blob], `selected_image_${Date.now()}.jpg`, { type: blob.type })
file.uid = genFileId()
if (props.limit === 1 && imageList.value.length === 1) {
imageList.value = []
}
uploadRef.value.handleStart(file)
uploadRef.value.submit()
}
defineExpose({
handleSelect,
triggerUpload
})
</script>
<style lang="less" scoped>
.image-uploader {
display: flex;
align-items: center;
}
.image-list {
display: flex;
gap: 8px;
align-items: center;
}
.image-item {
position: relative;
width: 56px;
height: 56px;
border-radius: 8px;
overflow: hidden;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
transform: scale(1.1);
z-index: 10;
.delete-icon {
opacity: 1;
}
}
.uploaded-image {
width: 100%;
height: 100%;
object-fit: cover;
transition: all 0.3s ease;
}
.image-index {
position: absolute;
bottom: 4px;
right: 4px;
width: 20px;
height: 20px;
background: rgba(0, 15, 51, 0.8);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 12px;
font-weight: bold;
z-index: 5;
transition: all 0.3s ease;
}
.delete-icon {
position: absolute;
top: -6px;
right: -6px;
width: 20px;
height: 20px;
background: #ef4444;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 12px;
cursor: pointer;
opacity: 0;
transition: all 0.3s ease;
z-index: 20;
&:hover {
background: #dc2626;
transform: scale(1.1);
}
}
}
.upload-trigger {
width: 56px;
height: 56px;
// border: 2px dashed #d1d5db;
background-color: #F8F9FA;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
cursor: pointer;
color: #9ca3af;
font-size: 15px;
transition: all 0.3s ease;
span {
color: #999;
font-family: "Microsoft YaHei";
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: normal;
}
&:hover {
background-color: #F0F1F2;
border-color: #626aef;
// color: #626aef;
transform: scale(1.05);
}
}
</style>

View File

@ -3,23 +3,20 @@
<div class="input-container" :class="{ generate : !props.isGenerate }" @click="handleContainerClick">
<div v-if="!props.isGenerate" class="title">AI绘画2026</div>
<div v-if="useDisplay.Sender_variant === 'default'" class="scroll-to-bottom-btn" @click.stop="handleScrollToBottom">
<div class="scroll-to-bottom-text">回到底部</div>
<div class="sender-top">
<div v-if="useDisplay.Sender_variant === 'default'" class="scroll-to-bottom-text" @click.stop="handleScrollToBottom">回到底部<img src="@/assets/dialog/ArrowDown.svg"></div>
<div class="upload-img-container">
<div class="reference-diagram">
<ImageUploader ref="referenceDiagramRef" v-model="referenceImages" :limit="4" />
</div>
</div>
</div>
<Sender :key="useDisplay.Sender_variant" v-model="prompt" :variant="useDisplay.Sender_variant" :placeholder="promptPlaceholder" :submit-btn-disabled="isgerenate.value" :auto-size="autoSizeConfig">
<template #prefix>
<div v-show="useDisplay.Sender_variant !== 'default'" class="prefix-self-wrap">
<div class="upload-btn">
<img src="@/assets/dialog/originalImage.svg" alt="" style="width: 16px;">
<span>上传原图</span>
</div>
<div class="upload-btn">
<img src="@/assets/dialog/referenceDiagram.svg" alt="" style="width: 16px;">
<span>上传参考图</span>
</div>
<Model v-model="model" />
<Model v-model="model" v-model:typeValue="type" />
<Proportion v-model="proportion" v-model:resolution="resolution" />
<Quantity v-model="quantity" />
</div>
@ -28,7 +25,7 @@
<template #action-list>
<div style="display: flex; align-items: center; gap: 8px; height: 100%;">
<el-button v-if="isgerenate" round color="#626aef" style="animation: spin 1s linear infinite;">
<!-- <i-ep-loading /> -->
<i-ep-loading />
</el-button>
<div v-else class="gerenate" :class="{ isprompt: prompt }" @click="handleStart">
<img v-if="!prompt" src="@/assets/dialog/darkArrow.svg" alt="" />
@ -39,19 +36,6 @@
</template>
</Sender>
<el-upload
v-show="false"
ref="uploadRef"
class="uploader"
:action="uploadurl"
multiple
:limit="1"
list-type="picture-card"
:before-upload="beforeUpload"
:on-success="SuccessSet"
:on-error="Errors"
:class="{ exceed: imageurl }"
/>
</div>
</Transition>
</template>
@ -60,8 +44,9 @@
import Proportion from './proportion/index.vue'
import Quantity from './quantity/index.vue'
import Model from './model/index.vue'
import ImageUploader from './imageUploader/index.vue'
import { Sender } from 'vue-element-plus-x'
import { useDisplayStore, useParamStore } from '@/stores'
import { useDisplayStore } from '@/stores'
import { generate } from '@/utils/websocket'
import { useRouter } from 'vue-router'
@ -77,19 +62,17 @@ const props = defineProps({
})
const router = useRouter()
const useDisplay = useDisplayStore()
const useParams = useParamStore()
const uploadurl = import.meta.env.VITE_API_WORKFLOW_UPLOAD
const uploadRef = ref(null)
const type = ref('image')
const prompt = ref('一个女孩在树下吃苹果')
const model = ref('flux')
const proportion = ref('9:16')
const proportion = ref('4:3')
const quantity = ref(1)
const resolution = ref('1k')
const promptPlaceholder = '描述你想生成的画面和动作。'
const prompt = ref('一个女孩在树下吃苹果')
const imageurl = ref('')
const imageurlShow = ref('')
const referenceImages = ref([])
const isgerenate = ref(false)
const autoSizeConfig = computed(() => {
@ -100,61 +83,6 @@ const autoSizeConfig = computed(() => {
}
})
//
const handleSelect = async (url) => {
imageurlShow.value = url
// URLBlob
const initialFile = await fetch(url)
const blob = await initialFile.blob()
//
const file = new File([blob], `selected_image_${Date.now()}.jpg`, { type: blob.type })
file.uid = genFileId()
console.log('file', file)
uploadRef.value.clearFiles() // 1
//
uploadRef.value.handleStart(file)
uploadRef.value.submit()
}
//
const beforeUpload = (rawFile) => {
console.log('beforeUpload', rawFile)
const allowedTypes = ['image/jpeg', 'image/png']
//
if (!allowedTypes.includes(rawFile.type)) {
// eslint-disable-next-line no-undef
ElMessage.error('不支持的文件类型,请上传 JPG、PNG 格式的图片')
return false
}
// 2MB
if (rawFile.size / 1024 / 1024 > 10) {
// eslint-disable-next-line no-undef
ElMessage.error('图片大小不能超过 10MB')
return false
}
}
// state
const SuccessSet = (response) => {
console.log('上传成功', response)
// eslint-disable-next-line no-undef
ElMessage.success('上传成功')
imageurl.value = response.url
}
//
const Errors = (error) => {
console.log('上传失败', error)
// eslint-disable-next-line no-undef
ElMessage.error('上传失败,请重新选择图片')
imageurlShow.value = ''
}
const handleStart = async () => {
if (!props.isGenerate) {
router.push({ name: 'home', query: { loading: false, Generate: true } })
@ -166,17 +94,25 @@ const handleStart = async () => {
}
isgerenate.value = true
console.log('生成开始', isgerenate.value)
const imgs = []
referenceImages.value.forEach((img, index) => {
imgs.push({ name: `image_${index + 1}`, data: img.url })
})
const data = {
AIGC: 'Painting',
platform: 'runninghub',
file_type: 'image',
modelName: model.value,
prompt: prompt.value,
quantity: quantity.value,
aspect_ratio: proportion.value,
resolution: resolution.value,
params: [
{ name: 'prompt', data: prompt.value},
{ name: 'quantity', data: quantity.value},
{ name: 'aspect_ratio', data: proportion.value},
{ name: 'resolution', data: resolution.value},
],
imgs
}
await generate('text', data)
await generate(type.value, data)
console.log('生成中', isgerenate.value)
}
@ -193,24 +129,14 @@ const handleScrollToBottom = () => {
watch(() => useDisplay.isSubGerenate, (newValue) => {
console.log('生成状态', newValue)
if (!newValue) {
console.log('生成完成', isgerenate.value)
isgerenate.value = useDisplay.isSubGerenate
}
isgerenate.value = newValue
// handleScrollToBottom()
}, { immediate: true })
watch(() => type.value, (newValue) => {
console.log('type.value', newValue)
})
watch(() => useParams.AIvideoImage, (newValue) => {
if (newValue) {
if (newValue.url === imageurl.value) return
console.log('图片选择成功,打开视频页面', newValue)
parentTime.value = newValue.time
parentIndex.value = newValue.parentIndex
parentTaskId.value = newValue.parentTaskId
imageurl.value = newValue.url
handleSelect(newValue.url)
useParams.AIvideoImage = ''
}
})
onMounted(() => {
if (props.generate) {
generate()
@ -231,7 +157,7 @@ onMounted(() => {
transition: all 0.3s ease;
}
.scroll-to-bottom-btn {
.sender-top {
width: 100%;
display: flex;
justify-content: flex-end;
@ -239,24 +165,48 @@ onMounted(() => {
transition: all 0.3s ease;
z-index: 101;
margin-bottom: 10px;
position: relative;
.scroll-to-bottom-text {
padding: 8px;
padding: 10px;
width: auto;
height: auto;
border-radius: 10px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
background-color: rgba(0, 15, 51, 0.836);
color: #ffffff;
// box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
background-color: #F8F9FA;
color: #666;
font-size: 12px;
display: flex;
align-items: center;
justify-content: center;
gap: 5px;
&:active {
transform: scale(0.95);
}
&:hover {
background-color: rgb(0, 15, 51);
background-color: #F0F1F2;
}
}
//
.upload-img-container{
position: absolute;
bottom: 0;
left: 0;
width: 80%;
display: flex;
justify-content: start;
align-items: center;
z-index: 1;
gap: 16px;
.reference-diagram {
display: flex;
align-items: center;
}
}
}
.generate{
height: 100%;
display: flex;

View File

@ -54,27 +54,34 @@ const props = defineProps({
modelValue: {
type: String,
default: 'flux'
},
typeValue: {
type: String,
default: 'text'
}
})
const emit = defineEmits(['update:modelValue'])
const emit = defineEmits(['update:modelValue', 'update:typeValue'])
const model = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
set: (value) => {
emit('update:modelValue', value)
const newType = getModelType(value)
emit('update:typeValue', newType)
}
})
const generateModels = [
{ value: 'flux', label: 'flux' },
{ value: 'Z-image', label: 'Z-image' },
{ value: 'zImage', label: 'Z-image' },
{ value: 'jimeng', label: 'jimeng' },
{ value: 'Qwen-image', label: 'Qwen-image' }
{ value: 'QwenImage', label: 'QwenImage' }
]
const editModels = [
{ value: 'Banana-Pro', label: 'Banana-Pro' },
{ value: 'BananaPro', label: 'Banana-Pro' },
{ value: 'Qwen-image', label: 'Qwen-image' },
{ value: 'Banana', label: 'Banana' },
{ value: 'Kontext', label: 'Kontext' },
{ value: 'Jimeng_4.0', label: 'Jimeng.4.0' }
]
@ -87,6 +94,19 @@ const selectModel = (value) => {
model.value = value
}
const getModelType = (value) => {
if (generateModels.find(m => m.value === value)) {
return 'text'
}
if (editModels.find(m => m.value === value)) {
return 'image'
}
if (visionModels.find(m => m.value === value)) {
return 'vision'
}
return 'text'
}
const getModelLabel = (value) => {
const allModels = [...generateModels, ...editModels, ...visionModels]
const model = allModels.find(item => item.value === value)

View File

@ -1,8 +1,42 @@
export function Playload(data, type) {
data = getWidthHeight(data)
console.log('宽与高', data)
if(data.modelName === 'flux'){
return flux(data)
export async function Playload(data,type) {
// data = getWidthHeight(data)
try {
const response = await fetch(`https://resources.xueai.art/AIGC/static/public/Platform/Painting/workflows/${type}/${data.modelName}.json`)
const json = await response.json()
const nodeInfoList = []
if (Array.isArray(data.imgs)) {
for (const key of data.imgs) {
if (json.nodeInfoList[key.name]) {
console.log(key)
json.nodeInfoList[key.name].fieldValue = key.url
nodeInfoList.push(json.nodeInfoList[key.name])
}
}
json.nodeInfoList[index].fieldValue = data.imgs.length() - 1
}
if (Array.isArray(data.params)) {
for (const key of data.params) {
if (json.nodeInfoList[key.name]) {
console.log(key)
json.nodeInfoList[key.name].fieldValue = key.data
nodeInfoList.push(json.nodeInfoList[key.name])
}
}
}
if (Array.isArray(json.must)) {
nodeInfoList.push(...json.must)
}
return {
workflowId: json.workflowId,
nodeInfoList
}
} catch (error) {
console.error('获取 JSON 文件失败:', error)
throw error
}
}
@ -32,46 +66,21 @@ function getWidthHeight(data) {
return data
}
function LTX2(data) {
const nodeInfoList = [
{ nodeId: '9', fieldName: 'text', fieldValue: data.prompt },
{ nodeId: '40', fieldName: 'text', fieldValue: data.aspect_ratio },
{ nodeId: '39', fieldName: 'text', fieldValue: data.resolution },
{ nodeId: '29', fieldName: 'index', fieldValue: '' }
]
switch (index){
case 1:
nodeInfoList[3].fieldValue = '1'
break
case 2:
nodeInfoList[3].fieldValue = '2'
break
case 3:
nodeInfoList[3].fieldValue = '3'
break
case 4:
nodeInfoList[3].fieldValue = '4'
break
}
if (data.image1) nodeInfoList.push({ nodeId: '2', fieldName: 'image', fieldValue: data.image1 })
if (data.image2) nodeInfoList.push({ nodeId: '3', fieldName: 'image', fieldValue: data.image2 })
if (data.image3) nodeInfoList.push({ nodeId: '4', fieldName: 'image', fieldValue: data.image3 })
if (data.image4) nodeInfoList.push({ nodeId: '13', fieldName: 'image', fieldValue: data.image4 })
if (data.image5) nodeInfoList.push({ nodeId: '14', fieldName: 'image', fieldValue: data.image5 })
return {
workflowId: '2031032712240304130',
nodeInfoList
}
}
function flux(data) {
const nodeInfoList = [
{ nodeId: '23', fieldName: 'text', fieldValue: data.prompt },
{ nodeId: '129', fieldName: 'aspect_ratio', fieldValue: data.aspect_ratio },
{ nodeId: '101', fieldName: 'control_after_generate', fieldValue: 'randomize' }
{ nodeId: '101', fieldName: 'control_after_generate', fieldValue: 'randomize' },
{ nodeId: '15', fieldName: 'index', fieldValue: 0 },
{nodeId: '2', fieldName: 'vae_name', fieldValue: 'ae.sft'},
{nodeId: '5', fieldName: 'sampler_name', fieldValue: 'euler'},
{nodeId: '50', fieldName: 'value', fieldValue: 1},
{nodeId: '102', fieldName: 'value', fieldValue: 20},
{nodeId: '103', fieldName: 'value', fieldValue: 1},
{nodeId: '104', fieldName: 'value', fieldValue: 1},
{nodeId: '105', fieldName: 'value', fieldValue: 0.8}
]
return {

View File

@ -2,6 +2,7 @@ const DisplayStoreSetup = () => {
const Sender_variant = ref('updown')
const scrollerRef = ref(null)
const tempList = ref([])
const isSubGerenate = ref(false)
const addGeneratingItem = (item) => {
const newItem = {
@ -63,6 +64,7 @@ const DisplayStoreSetup = () => {
Sender_variant,
scrollerRef,
tempList,
isSubGerenate,
addGeneratingItem,
updateItemToSuccess,
initHistoryList,

View File

@ -3,7 +3,7 @@ import outPlatform from '@/config/index'
// 处理音频生成任务的数据并返回
export async function createTask(data, type, taskId, token) {
console.log(data, type)
const payload = await outPlatform[data.platform].Playload(data)
const payload = await outPlatform[data.platform].Playload(data, type)
return {
AIGC: data.AIGC,

View File

@ -55,11 +55,13 @@ export async function generate(type, data) {
const token = getToken()
const taskId = crypto.randomUUID()
let currentTaskId = null
useDisplay.isSubGerenate = true
const result = await createTask(data, type, taskId, token)
console.log(result)
// const token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpblR5cGUiOiJsb2dpbiIsImxvZ2luSWQiOjY0NDEwODAyMjk1OTgzNzIzMCwicm5TdHIiOiJiWkVwS2JLWFJyZmRIaFFHWXZKTkdzOGdGM0JSRmxQOCJ9.5eQ2GtVdrDntQDe2tnF8vl_DhTfd2uW-KNqzvl1imc0'
const wsURL = `${import.meta.env.VITE_API_WORKFLOW_WS}/?token=${token}`
const wsURL = `${import.meta.env.VITE_API_WORKFLOW_WS}/?token=Bearer ${token}`
const socket = new WebSocket(wsURL)
console.log('WebSocket连接已建立')
@ -94,7 +96,9 @@ export async function generate(type, data) {
name: data.prompt || '生成中...',
type: type
})
setTimeout(() => {
useDisplay.scrollToBottom()
}, 100)
return
}
message.value = event.data
@ -121,7 +125,7 @@ export async function generate(type, data) {
// 处理链接关闭
socket.onclose = async (event) => {
console.log('WebSocket已关闭:', event)
useDisplay.isSubGerenate = false
// 清理心跳定时器
if (heartbeatInterval) {
clearInterval(heartbeatInterval)

View File

@ -365,9 +365,11 @@ const addCollection = (url) => {
align-items: center;
justify-content: center;
gap: 5px;
padding: 5px 10px;
border-radius: 5px;
width: 120px;
height: 36px;
border-radius: 10px;
cursor: pointer;
background-color: #F8F9FA;
}
.bottom-btn:hover{
background-color: #e4e7ed;

View File

@ -1,389 +0,0 @@
<template>
<div id="display" class="content-area">
<RefreshOverlay :visible="refreshing" />
<div class="back">
<img src="@/assets/display/back.svg" alt="">
<span class="title-text">退出</span>
</div>
<div v-if="props.if" class="btn-container">
<div class="btn">
<!-- <span class="btn-text">全部</span> -->
<img src="@/assets/display/search.svg" alt="">
</div>
<span class="line"></span>
<div class="btn">
<Select v-model="selectedTime" :options="timeOptions" width="auto">
<template #prefix>
<i-ep-Calendar />
</template>
<template #header>
<div class="header">
<el-date-picker
v-model="value1"
type="daterange"
start-placeholder="Start date"
end-placeholder="End date"
:size="size"
/>
</div>
</template>
</Select>
</div>
<span class="line"></span>
<div class="btn">
<Select v-model="selectedFavorite" :options="favoriteOptions" width="auto" >
<template #prefix>
<i-ep-Star />
</template>
</Select>
</div>
</div>
<DynamicScroller
ref="scrollerRef"
v-if="props.if"
:items="list"
:min-item-size="54"
class="scroller"
:buffer="250"
@scroll="handleScroll"
>
<template #default="{ item, index, active }">
<DynamicScrollerItem
:item="item"
:active="active"
:data-index="index"
>
<Set :key="`${item.id}`" :item="item" />
</DynamicScrollerItem>
</template>
</DynamicScroller>
</div>
</template>
<script setup>
import { useDisplayStore, useParamStore, useUserStore } from '@/stores'
import { storeToRefs } from 'pinia'
import Set from './components/set.vue'
import RefreshOverlay from './components/RefreshOverlay.vue'
import Select from '@/components/Select/index.vue'
import { getGenerateHistoryList } from '@/apis/display'
import { useRouter } from 'vue-router'
const props = defineProps({
if: {
type: Boolean,
default: false
},
loading: {
type: Boolean,
default: false
}
})
const useDisplay = useDisplayStore()
const useParams = useParamStore()
const userStore = useUserStore()
const router = useRouter()
const refreshing = ref(false)
const scrollerRef = ref(null)
const isLoadingMore = ref(false)
const activeTab = ref('all')
const isInitializing = ref(true)
let total = 0
const timeOptions = [
{ label: '全部', value: 'all' },
{ label: '最近一周', value: 'week' },
{ label: '最近一个月', value: 'month' },
{ label: '最近三个月', value: 'quarter' }
]
const favoriteOptions = [
{ label: '全部', value: 'all' },
{ label: '已收藏', value: 'favorite' }
]
const selectedTime = ref('all')
const selectedFavorite = ref('all')
const { tempList } = storeToRefs(useDisplay)
// const tempList = ref([
// { id: 0, type: 'image', status: 'none', name: '', time: '2025-12-01 18:26', files: [] },
// { id: 1, type: 'image', status: 'success', name: '', time: '2025-12-01 18:26', parentName: '2', parentTime: '2025-12-01 12:26', files: ['https://sxwz.xueai.art/static/png/image4-1-1-BZWQeEAk.png', 'https://sxwz.xueai.art/static/png/image4-1-1-BZWQeEAk.png'] },
// { id: 2, type: 'image', status: 'success', name: '', time: '2025-12-01 18:26', parentName: '2', parentTime: '2025-12-01 12:26', files: ['https://sxwz.xueai.art/static/png/image4-1-1-BZWQeEAk.png', 'https://sxwz.xueai.art/static/png/image4-1-1-BZWQeEAk.png', 'https://sxwz.xueai.art/static/png/image4-1-1-BZWQeEAk.png', 'https://sxwz.xueai.art/static/png/image4-1-1-BZWQeEAk.png'] },
// { id: 3, type: 'image', status: 'success', name: '', time: '2025-12-01 18:26', parentName: '2', parentTime: '2025-12-01 12:26', files: ['https://sxwz.xueai.art/static/png/image4-1-1-BZWQeEAk.png', 'https://sxwz.xueai.art/static/png/image4-1-1-BZWQeEAk.png', 'https://sxwz.xueai.art/static/png/image4-1-1-BZWQeEAk.png', 'https://sxwz.xueai.art/static/png/image4-1-1-BZWQeEAk.png'] },
// { id: 4, type: 'image', status: 'success', name: '', time: '2025-12-01 18:26', parentName: '2', parentTime: '2025-12-01 12:26', files: ['https://sxwz.xueai.art/static/png/image4-1-1-BZWQeEAk.png', 'https://sxwz.xueai.art/static/png/image4-1-1-BZWQeEAk.png', 'https://sxwz.xueai.art/static/png/image4-1-1-BZWQeEAk.png', 'https://sxwz.xueai.art/static/png/image4-1-1-BZWQeEAk.png'] }
// ])
const activeFilter = ref('all')
const list = computed(() => {
const data = tempList.value || []
if (activeFilter.value === 'all') {
return data
}
return data.filter((item) => item.type === activeFilter.value)
})
const page = ref(1)
//
const toggleDisplay = (newValue, oldValue) => {
activeFilter.value = newValue
}
//
const conversion = (newlist) => {
const temp = newlist.data.records.map((item) => {
return {
id: item.taskId,
collection: item.collection,
status: 'success',
prompt: item.prompt,
params: item.params,
time: item.createTime,
files: [item.fileUrl]
}
})
return temp
}
//
const fetchHistory= async (isScrollTopLoad = false) => {
try {
if (isScrollTopLoad) {
return
}
const result = await getGenerateHistoryList({ userId: userStore.userInfo.id, chargeType: 1 })
total = result.data ? result.data.length : 0
if (total === 0) {
useDisplay.Sender_variant = 'updown'
router.push({ name: 'home' })
}
const wrappedData = {
data: {
records: result.data
}
}
const convertedList = conversion(wrappedData)
const adaptedList = convertedList.map((item, index) => {
const originalItem = result.data[index]
return {
...item,
text: originalItem?.title || item.prompt || '生成图片',
name: originalItem?.title || item.prompt || '生成图片',
type: 'image',
title: originalItem?.title || '生成图片'
}
})
if (!isScrollTopLoad && adaptedList.length > 0) {
useDisplay.initHistoryList(adaptedList)
await nextTick()
const scrollToBottomDirect = (force = false) => {
if (scrollerRef.value) {
const el = scrollerRef.value.$el
if (el) {
const viewport = el.querySelector('.vue-recycle-scroller__viewport')
if (viewport) {
console.log('直接滚动 - scrollHeight:', viewport.scrollHeight, 'force:', force)
if (force) {
viewport.scrollTop = viewport.scrollHeight + 1000
} else {
viewport.scrollTop = viewport.scrollHeight
}
}
}
if (typeof scrollerRef.value.scrollToItem === 'function') {
console.log('直接 scrollToItem')
scrollerRef.value.scrollToItem(list.value.length - 1)
}
}
}
for (let i = 0; i < 20; i++) {
setTimeout(() => {
scrollToBottomDirect(i >= 15)
}, 60 * i)
}
setTimeout(() => {
scrollToBottomDirect(true)
setTimeout(() => {
refreshing.value = false
isInitializing.value = false
useDisplay.scrollToBottom()
}, 600)
}, 1500)
} else {
useDisplay.initHistoryList(adaptedList)
}
} catch (error) {
console.error('获取历史失败:', error)
ElMessage({
message: '获取历史失败',
type: 'warning'
})
}
}
//
const getList = async () => {
if (isLoadingMore.value) return
isLoadingMore.value = true
try {
await fetchHistory(true)
} finally {
isLoadingMore.value = false
}
}
//
const handleScroll = (event) => {
if (isInitializing.value) return
const { scrollTop, scrollHeight, clientHeight } = event.target
const distanceToBottom = scrollHeight - scrollTop - clientHeight
//
// if (scrollTop <= 50 && !isLoadingMore.value) {
// getList()
// }
if (distanceToBottom <= 50) {
useDisplay.Sender_variant = 'updown'
} else if (distanceToBottom >= 350) {
useDisplay.Sender_variant = 'default'
}
}
onMounted(() => {
console.log('display 组件已挂载')
if (!props.loading) return
refreshing.value = true
nextTick(() => {
console.log('设置 scrollerRef 到 store')
useDisplay.scrollerRef = scrollerRef.value
fetchHistory()
})
page.value++
})
</script>
<style lang="less" scoped>
.content-area {
width: 100%;
min-width: 750px;
height: 100%;
overflow-y: auto;
transition: all 0.3s ease;
}
.back{
display: flex;
align-items: center;
gap: 6px;
height: 36px;
padding: 10px;
position: absolute;
left: 30px;
top: 22px;
z-index: 3;
cursor: pointer;
border-radius: 10px;
// background-color: #FAFBFC;
}
.back:hover{
background-color: #e4e7ed;
}
.btn-container{
display: flex;
align-items: center;
gap: 6px;
height: auto;
padding: 4px;
right: 30px;
top: 22px;
z-index: 3;
border-radius: 10px;
background-color: #FAFBFC;
position: absolute;
.btn{
display: flex;
padding: 5px;
align-items: center;
gap: 5px;
}
.btn-text{
color: #000;
font-family: "Microsoft YaHei";
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 18px;
text-align: center;
}
.line{
width: 1px;
height: 8px;
background-color: #ccc;
}
}
.scroller {
height: 100%;
padding: 30px 0px 200px 0px;
will-change: scroll-position;
-webkit-overflow-scrolling: touch; /* iOS Safari */
scroll-behavior: smooth; /* 平滑滚动 */
&::-webkit-scrollbar-track {
background: transparent; /* 轨道透明 */
}
:deep(.option-item) {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 0 20px;
cursor: pointer;
transition: all 0.3s ease;
}
:deep(.option-item:hover) {
background-color: #f5f7fa;
}
:deep(.option-item.selected) {
color: #000F33;
font-weight: 500;
}
:deep(.option-text) {
flex: 1;
text-align: left;
}
:deep(.option-check) {
margin-left: 10px;
font-weight: bold;
}
}
</style>