新逻辑
This commit is contained in:
parent
e6290b53e5
commit
c9ed299ef6
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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']
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -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) => {
|
||||
// 从URL获取图片Blob对象
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
// 从URL获取图片Blob对象
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
Loading…
Reference in New Issue