绘画初步完成所有功能逻辑,右上角时间与收藏筛选待完成

This commit is contained in:
王佑琳 2026-03-31 18:51:26 +08:00
parent 727ecb378b
commit 5a7dab7dc7
13 changed files with 277 additions and 589 deletions

2
components.d.ts vendored
View File

@ -11,7 +11,9 @@ export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
2: typeof import('./src/components/virtual-scroller/VirtualScroller copy 2.vue')['default']
Canvas: typeof import('./src/components/canvas/index.vue')['default']
copy: typeof import('./src/components/virtual-scroller/VirtualScroller copy.vue')['default']
DialogBox: typeof import('./src/components/dialogBox/index.vue')['default']
ElButton: typeof import('element-plus/es')['ElButton']
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']

View File

@ -159,6 +159,7 @@
<script setup>
import { generate } from '@/utils/websocket'
import { useDisplayStore } from '@/stores'
import request from '@/utils/request'
const props = defineProps({
visible: {
@ -176,6 +177,10 @@ const props = defineProps({
source: {
type: String,
default: ''
},
type: {
type: String,
default: ''
}
})
@ -236,15 +241,15 @@ const updateCurrentEditingContent = (description) => {
const shapeWord = shapeType === 'circle' ? '圈' : '框'
if (selectedReferenceImages.value.length > 0) {
const imageIndex = allReferenceImages.value.indexOf(selectedReferenceImages.value[0]) + 1
const imageIndex = allReferenceImages.value.indexOf(selectedReferenceImages.value[0]) + 2
const prefix = isFirstShape ? '' : ''
currentEditingContent.value = `${prefix}${colorName}${shapeWord}内的【XXX】替换为【图${imageIndex}中的${description}`
currentEditingContent.value = `${prefix}图1${colorName}${shapeWord}内的【XXX】替换为【图${imageIndex}中的${description}`
} else if (description) {
const prefix = isFirstShape ? '' : ''
currentEditingContent.value = `${prefix}${colorName}${shapeWord}内的【XXX】替换为【${description}`
currentEditingContent.value = `${prefix}图1${colorName}${shapeWord}内的【XXX】替换为【${description}`
} else {
const prefix = isFirstShape ? '' : ''
currentEditingContent.value = `${prefix}${colorName}${shapeWord}内的【XXX】替换为【XXX】或【图X中的XXX】`
currentEditingContent.value = `${prefix}图1${colorName}${shapeWord}内的【XXX】替换为【XXX】或【图X中的XXX】`
}
}
@ -281,7 +286,13 @@ const initCanvas = () => {
const containerHeight = container.clientHeight
if (currentImage.value) {
if (currentImage.value.startsWith('data:')) {
let imageUrl = currentImage.value
if (!imageUrl.startsWith('data:')) {
imageUrl = imageUrl.replace('https://sxwz.xueai.art', 'https://talkingdraw.xueai.art')
}
if (imageUrl.startsWith('data:')) {
const img = new Image()
img.onload = () => {
const imgScale = Math.min(containerWidth / img.width, containerHeight / img.height)
@ -291,9 +302,9 @@ const initCanvas = () => {
bgImage.value = img
scale.value = imgScale
}
img.src = currentImage.value
img.src = imageUrl
} else {
fetch(currentImage.value)
fetch(imageUrl)
.then(res => res.blob())
.then(blob => {
const img = new Image()
@ -548,18 +559,55 @@ const handleSend = async () => {
imgs.push({ name: `image_${index + 2}`, url: img })
})
const uploadImg = async (imgItem) => {
if (!imgItem.url.startsWith('data:') && !imgItem.url.startsWith('blob:')) {
return imgItem
}
const response = await fetch(imgItem.url)
const blob = await response.blob()
const file = new File([blob], `${imgItem.name}.png`, { type: 'image/png' })
const formData = new FormData()
formData.append('file', file)
try {
const result = await request({
url: import.meta.env.VITE_API_WORKFLOW_UPLOAD,
method: 'POST',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
})
if (result.success || result.code === 0) {
return { name: imgItem.name, url: result.url }
}
return imgItem
} catch (error) {
console.error('上传失败:', error)
return imgItem
}
}
const uploadedImgs = await Promise.all(imgs.map(uploadImg))
const generateData = {
model: 'banana',
modelType: 'edit',
prompt: inputText.value,
proportion: '比例自动'
}
const data = {
AIGC: 'Painting',
platform: 'runninghub',
file_type: 'image',
modelName: 'flux',
modelName: 'banana',
params: [
{ name: 'prompt', data: inputText.value },
{ name: 'quantity', data: 1 },
{ name: 'aspect_ratio', data: '16:9' },
{ name: 'resolution', data: '1k' }
{ name: 'prompt', data: inputText.value + '并且去除掉图1中的框' },
{ name: 'index', data: 1 },
],
imgs
imgs: uploadedImgs,
result: JSON.stringify(generateData)
}
emit('send', {
@ -568,7 +616,7 @@ const handleSend = async () => {
shapes: shapes.value
})
await generate('text', data)
await generate('edit', data , generateData, props.type)
handleClose()
}
@ -592,7 +640,7 @@ const handleUploadReference = () => {
input.type = 'file'
input.accept = 'image/*'
input.multiple = true
input.onchange = (e) => {
input.onchange = async (e) => {
const files = Array.from(e.target.files)
const remainingSlots = 5 - allReferenceImages.value.length
@ -603,13 +651,17 @@ const handleUploadReference = () => {
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)
})
const readFileAsDataURL = (file) => {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = (event) => resolve(event.target.result)
reader.onerror = (error) => reject(error)
reader.readAsDataURL(file)
})
}
const imageDataUrls = await Promise.all(filesToUpload.map(readFileAsDataURL))
allReferenceImages.value.push(...imageDataUrls)
}
input.click()
}
@ -818,11 +870,6 @@ const handleBrushConfirm = () => {
cursor: pointer;
transition: all 0.3s;
&:hover {
// border-color: #409eff;
// color: #409eff;
}
&.active {
color: #fff;
@ -847,10 +894,6 @@ const handleBrushConfirm = () => {
cursor: pointer;
transition: all 0.3s;
&:hover {
// border-color: #409eff;
// color: #409eff;
}
}
}
}
@ -867,6 +910,7 @@ const handleBrushConfirm = () => {
font-style: normal;
font-weight: 400;
line-height: normal;
cursor: pointer;
}
.brush-panel {

View File

@ -40,6 +40,9 @@ const props = defineProps({
type: Number,
default: 1
},
/**
* 图片列表每个元素包含 url uid 属性
*/
modelValue: {
type: Array,
default: () => []
@ -52,13 +55,39 @@ const uploadurl = import.meta.env.VITE_API_WORKFLOW_UPLOAD
const uploadRef = ref(null)
const imageList = ref([...props.modelValue])
const localPreviewList = ref([...props.modelValue])
const isUploading = ref(false)
watch(() => props.modelValue, (newVal) => {
imageList.value = [...newVal]
if (newVal.length === 0) {
localPreviewList.value = []
watch(() => props.modelValue, async (newVal) => {
if (isUploading.value) {
return
}
}, { deep: true })
imageList.value = [...newVal]
const newPreviewList = []
for (const img of newVal) {
let previewImg = { ...img }
if (img.url && !img.url.startsWith('blob:')) {
try {
const response = await fetch(img.url)
const blob = await response.blob()
previewImg = {
...img,
url: URL.createObjectURL(blob),
serverUrl: img.url
}
} catch (error) {
console.error('Failed to create blob URL:', error)
previewImg = {
...img,
serverUrl: img.url
}
}
}
newPreviewList.push(previewImg)
}
localPreviewList.value = newPreviewList
}, { deep: true, immediate: true })
const triggerUpload = () => {
uploadRef.value.$el.querySelector('input').click()
@ -81,6 +110,8 @@ const beforeUpload = (rawFile) => {
const handleSuccess = (response, uploadFile) => {
ElMessage.success('上传成功')
isUploading.value = true
const localUrl = URL.createObjectURL(uploadFile.raw)
const newImage = {
@ -92,9 +123,14 @@ const handleSuccess = (response, uploadFile) => {
const newPreview = {
uid: uploadFile.uid,
url: localUrl
url: localUrl,
serverUrl: response.url
}
localPreviewList.value.push(newPreview)
nextTick(() => {
isUploading.value = false
})
}
const handleError = () => {
@ -107,7 +143,7 @@ const handleExceed = () => {
const handleDelete = (index) => {
const previewItem = localPreviewList.value[index]
if (previewItem && previewItem.url.startsWith('blob:')) {
if (previewItem && previewItem.url && previewItem.url.startsWith('blob:')) {
URL.revokeObjectURL(previewItem.url)
}
@ -117,10 +153,10 @@ const handleDelete = (index) => {
}
const handleImageClick = (clickedIndex) => {
const clickedImage = imageList.value[clickedIndex]
const clickedImage = localPreviewList.value[clickedIndex]
if (!clickedImage) return
const otherImages = imageList.value
const otherImages = localPreviewList.value
.filter((_, index) => index !== clickedIndex)
.map((img, index) => ({
...img,

View File

@ -106,6 +106,7 @@ const autoSizeConfig = computed(() => {
const handleStart = async () => {
const currentType = props.type
let currentModelType = modelType.value
if (!props.isGenerate) {
router.push({ name: 'home', query: { loading: false, Generate: true, type: currentType } })
@ -121,11 +122,19 @@ const handleStart = async () => {
referenceImages.value.forEach((img, index) => {
imgs.push({ name: `image_${index + 1}`, url: img.url })
})
console.log('imgs', imgs)
// console.log('imgs', imgs)
//
if (currentType === 'video') {
if (imgs.length > 0) {
currentModelType = 'image'
} else {
currentModelType = 'text'
}
}
const generateData = {
model: model.value,
modelType: modelType.value,
modelType: currentModelType,
prompt: prompt.value,
proportion: proportion.value,
referenceImages: referenceImages.value,
@ -138,7 +147,6 @@ const handleStart = async () => {
const data = {
AIGC: 'Painting',
platform: 'runninghub',
file_type: 'image',
modelName: model.value,
params: [
{ name: 'prompt', data: prompt.value},
@ -149,7 +157,7 @@ const handleStart = async () => {
imgs,
result: JSON.stringify(generateData)
}
await generate(modelType.value, data, generateData, props.type)
await generate(currentModelType, data, generateData, currentType)
console.log('生成中', isgerenate.value)
}
@ -236,11 +244,12 @@ onMounted(() => {
position: relative;
.scroll-to-bottom-text {
position: absolute;
bottom: 0;
right: 0;
z-index: 2;
padding: 10px;
width: auto;
height: auto;
border-radius: 10px;
// box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
background-color: #F8F9FA;
color: #666;
font-size: 12px;
@ -248,6 +257,7 @@ onMounted(() => {
align-items: center;
justify-content: center;
gap: 5px;
&:active {
transform: scale(0.95);
}

View File

@ -1,12 +1,12 @@
export async function Playload(data,type) {
export async function Playload(data,modelType) {
// data = getWidthHeight(data)
try {
const response = await fetch(`https://resources.xueai.art/AIGC/static/public/Platform/Painting/workflows/${type}/${data.modelName}.json`)
const response = await fetch(`https://resources.xueai.art/AIGC/static/public/Platform/Painting/workflows/${modelType}/${data.modelName}.json`)
const json = await response.json()
const nodeInfoList = []
if (Array.isArray(data.imgs) && data.imgs.length > 0 && type === 'image') {
if (Array.isArray(data.imgs) && data.imgs.length > 0 && (modelType === 'image' || modelType === 'edit')) {
for (const key of data.imgs) {
if (json.nodeInfoList[key.name]) {
console.log(key)

View File

@ -1,15 +1,15 @@
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, type)
export async function createTask(data, modelType, taskId, token) {
console.log(data, modelType)
const payload = await outPlatform[data.platform].Playload(data, modelType)
return {
AIGC: data.AIGC,
platform: data.platform,
prompt: data.prompt,
taskType: type === 'text' ? 1 : 2,
taskType: modelType === 'text' ? 1 : 2,
modelName: data.modelName,
payload,
taskId,

View File

@ -1,15 +1,21 @@
<template>
<div style="width: 100%;display: flex;justify-content: center;align-items: center;transform: rotate(180deg);">
<div class="primary-box" :class="{ 'none-primary-box': props.item.status === 'none' }">
<!-- 标题 -->
<div class="title" ref="titleRef" @mouseenter="isHovering = true" @mouseleave="isHovering = false">
<div class="prompt-wrapper">
<div class="prompt" ref="nameRef" :class="{ 'expanded': isHovering }">{{ props.item.generateData.prompt || '生成图片' }}</div>
<div class="generate-data" v-show="!isHovering" ref="generateDa taRef" :class="{ 'second-line': shouldShowOnSecondLine }">
<div class="detailed-data first-detailed-data">{{ props.item.generateData.model }}</div>
<div class="detailed-data">{{ props.item.generateData.proportion }}</div>
<div class="prompt-container" ref="promptContainerRef" @mouseenter="isHovering = true" @mouseleave="isHovering = false">
<div class="prompt-wrapper" ref="promptWrapperRef">
<div class="prompt" ref="promptRef" :class="{ 'expanded': isHovering }">
<span class="prompt-text">{{ props.item.generateData.prompt || '生成图片' }}</span>
<div class="generate-data internal" v-show="!isHovering && !showExternalGenerateData">
<div class="detailed-data first-detailed-data">{{ props.item.generateData.model }}</div>
<div class="detailed-data">{{ props.item.generateData.proportion }}</div>
</div>
</div>
</div>
<div class="generate-data external" v-show="!isHovering && showExternalGenerateData">
<div class="detailed-data first-detailed-data">{{ props.item.generateData.model }}</div>
<div class="detailed-data">{{ props.item.generateData.proportion }}</div>
</div>
</div>
<!-- 加载中 -->
@ -114,45 +120,36 @@ const useUser = useUserStore()
const localCollectStatus = ref({ ...props.item.collectStatus })
const hoverIndex = ref(-1)
const isHovering = ref(false)
const shouldShowOnSecondLine = ref(false)
const nameRef = ref(null)
const titleRef = ref(null)
const generateDataRef = ref(null)
const wrapperHeight = ref(38.5)
const showExternalGenerateData = ref(false)
const promptContainerRef = ref(null)
const promptWrapperRef = ref(null)
const promptRef = ref(null)
const checkTextOverflow = () => {
nextTick(() => {
if (nameRef.value && titleRef.value && generateDataRef.value) {
if (promptRef.value && promptWrapperRef.value && promptContainerRef.value) {
const lineHeight = 22.5
const padding = 8
const twoLineHeight = 55
const generateDataWidth = generateDataRef.value.offsetWidth + 20
const twoLineHeight = lineHeight * 2 + padding
nameRef.value.style.maxHeight = 'none'
nameRef.value.style.overflow = 'visible'
promptRef.value.style.maxHeight = 'none'
promptRef.value.style.overflow = 'hidden'
const actualHeight = nameRef.value.scrollHeight
const lineCount = Math.ceil((actualHeight - padding * 2) / lineHeight)
const actualHeight = promptRef.value.scrollHeight
const lineCount = Math.ceil((actualHeight - padding) / lineHeight)
if (!isHovering.value) {
nameRef.value.style.maxHeight = '55px'
nameRef.value.style.overflow = 'hidden'
promptRef.value.style.maxHeight = `${twoLineHeight}px`
promptRef.value.style.overflow = 'hidden'
}
if (lineCount > 1) {
shouldShowOnSecondLine.value = true
wrapperHeight.value = twoLineHeight
} else {
const containerWidth = titleRef.value.offsetWidth
const promptWidth = nameRef.value.scrollWidth
const firstLineRemaining = containerWidth - promptWidth
if (firstLineRemaining < generateDataWidth) {
shouldShowOnSecondLine.value = true
wrapperHeight.value = twoLineHeight
showExternalGenerateData.value = lineCount >= 3
if (!isHovering.value) {
if(lineCount >= 3){
promptContainerRef.value.style.height = `${twoLineHeight}px`
} else {
shouldShowOnSecondLine.value = false
wrapperHeight.value = Math.max(lineHeight + padding * 2, actualHeight)
promptContainerRef.value.style.height = `${actualHeight}px`
}
}
}
@ -160,13 +157,17 @@ const checkTextOverflow = () => {
}
watch(isHovering, (newVal) => {
if (nameRef.value) {
if (promptRef.value) {
const lineHeight = 22.5
const padding = 8
const twoLineHeight = lineHeight * 2 + padding
if (newVal) {
nameRef.value.style.maxHeight = 'none'
nameRef.value.style.overflow = 'visible'
promptRef.value.style.maxHeight = 'none'
promptRef.value.style.overflow = 'visible'
} else {
nameRef.value.style.maxHeight = '55px'
nameRef.value.style.overflow = 'hidden'
promptRef.value.style.maxHeight = `${twoLineHeight}px`
promptRef.value.style.overflow = 'hidden'
}
}
})
@ -204,11 +205,19 @@ const AIbrush = (file, index) => {
}
const reEdit = () => {
if(props.item.generateData?.modelType === 'edit'){
ElMessage.error('画笔生成的任务不能重新编辑')
return
}
useDisplay.setResultData(props.item.generateData)
useDisplay.fillParamsForEdit()
}
const againGenerate = () => {
if(props.item.generateData?.modelType === 'edit'){
ElMessage.error('画笔生成的任务不能再次生成')
return
}
useDisplay.setResultData(props.item.generateData)
useDisplay.triggerGenerateWithResult()
}
@ -293,77 +302,97 @@ const addCollection = async (url) => {
height: 350px;
}
.title{
.prompt-container{
width: 100%;
position: relative;
overflow: visible;
}
.prompt-wrapper{
position: relative;
min-height: 28.5px;
overflow: visible;
.prompt-wrapper{
position: relative;
width: 100%;
}
.prompt{
color: #333;
font-family: "Microsoft YaHei";
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22.5px;
word-break: break-all;
padding: 8px;
background-color: #fff;
position: absolute;
left: -8px;
top: 0;
width: 102%;
z-index: 5;
&.expanded{
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
border-radius: 4px;
}
}
.prompt{
position: absolute;
left: 0;
top: 0;
color: #333;
font-family: "Microsoft YaHei";
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22.5px;
word-break: break-all;
max-height: 60px;
overflow: hidden;
z-index: 10;
padding: 8px;
background-color: #fff;
&.expanded{
max-height: none;
overflow: visible;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
border-radius: 4px;
}
}
.prompt-text{
display: inline;
color: #333;
font-family: "Microsoft YaHei";
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: normal;
}
.generate-data{
position: absolute;
right: 0;
top: 0;
.generate-data{
display: inline-flex;
align-items: center;
background-color: #fff;
height: 22.5px;
text-align: center;
padding: 0 12px;
color: #999;
font-family: "Microsoft YaHei";
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: normal;
&.internal{
display: inline-flex;
align-items: center;
z-index: 2;
background-color: #fff;
padding-right: 20px;
&.second-line{
top: auto;
bottom: 0;
}
margin-left: 10px;
}
&.external{
position: absolute;
right: -16px;
bottom: 0;
z-index: 6;
// padding-right: 20px;
}
}
.detailed-data{
display: inline-flex;
align-items: center;
justify-content: center;
color: #999;
font-family: "Microsoft YaHei";
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: normal;
padding: 0 10px;
border-left: 1px solid #999;
white-space: nowrap;
height: 12px;
}
.detailed-data{
display: inline-flex;
align-items: center;
justify-content: center;
color: #999;
font-family: "Microsoft YaHei";
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: normal;
padding: 0 10px;
border-left: 1px solid #999;
white-space: nowrap;
height: 12px;
}
.first-detailed-data{
border-left: none;
padding-left: 0;
}
.first-detailed-data{
border-left: none;
padding-left: 0;
}
.box{

View File

@ -1,391 +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="800"
class="scroller"
:buffer="50"
@scroll="handleScroll"
>
<template #default="{ item, index, active }">
<DynamicScrollerItem
:key="item.id"
:item="item"
:active="active"
:index="index"
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 350px 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>

View File

@ -1,7 +1,7 @@
<template>
<div id="display" class="content-area">
<RefreshOverlay :visible="refreshing" />
<Canvas v-model:visible="canvasVisible" :image="canvasImage" :reference-images="canvasReferenceImages" :source="canvasSource" @send="handleCanvasSend" />
<Canvas v-model:visible="canvasVisible" :image="canvasImage" :reference-images="canvasReferenceImages" :source="canvasSource" :type="props.type" @send="handleCanvasSend" />
<div class="back" @click="handleExit">
<img src="@/assets/display/back.svg" alt="">

View File

@ -1,10 +0,0 @@
{
"nodeInfoList": {
"value_1":{ "nodeId":"20", "fieldName":"value", "fieldValue":"512" },
"value_2":{ "nodeId":"21", "fieldName":"value", "fieldValue":"512" },
"model_type":{ "nodeId":"18", "fieldName":"model_type", "fieldValue":"pro" },
"audio":{ "nodeId":"16", "fieldName":"audio", "fieldValue":"dce37fba29e596ddcd927c4660b4fb47bd3ecdbef4ad242fed72bd860e032b1f.flac" },
"image":{ "nodeId":"17", "fieldName":"image", "fieldValue":"d3ee810ce387739e7a99cb3ba87a104e34b0a955149b8525a600867edbab1138.png" }
},
"workflowId": "2036266399357739009"
}

View File

@ -1,11 +0,0 @@
{
"nodeInfoList": {
"text":{ "nodeId":"40", "fieldName":"text", "fieldValue":"深夜,一个美丽的中年中国女人在一边弹吉他一边歌唱,环绕镜头,半身特写,逆光,月光,海风吹拂。场景是海边。" },
"audio":{ "nodeId":"39", "fieldName":"audio", "fieldValue":"62e5c0b15854bcac34e9aa0bf9f449767bda9f66047a60abeddbcd47c712ee8d.mp3" },
"start_index":{ "nodeId":"58", "fieldName":"start_index", "fieldValue":0 },
"duration":{ "nodeId":"58", "fieldName":"duration", "fieldValue":25 },
"value_1":{ "nodeId":"55", "fieldName":"value", "fieldValue":1280 },
"value_2":{ "nodeId":"56", "fieldName":"value", "fieldValue":720 }
},
"workflowId": "2036343285949665282"
}

View File

@ -1,10 +0,0 @@
{
"nodeInfoList": {
"image":{ "nodeId":"2", "fieldName":"image", "fieldValue":"67bbf03a4ce453557b8c9acf85bd83d3519d3374ef35c54da1084d03f9ac111f.png" },
"prompt":{ "nodeId":"3", "fieldName":"prompt", "fieldValue":"一个小女孩在树下吃苹果" },
"resolution":{ "nodeId":"3", "fieldName":"resolution", "fieldValue":"540p" },
"duration":{ "nodeId":"3", "fieldName":"duration", "fieldValue":5 },
"audio":{ "nodeId":"3", "fieldName":"audio", "fieldValue":true }
},
"workflowId": "2036354451904139265"
}

View File

@ -1,11 +0,0 @@
{
"nodeInfoList": {
"style":{ "nodeId":"2", "fieldName":"style", "fieldValue":"general" },
"prompt":{ "nodeId":"2", "fieldName":"prompt", "fieldValue":"一个小女孩在树下吃苹果" },
"resolution":{ "nodeId":"2", "fieldName":"resolution", "fieldValue":"540p" },
"aspect_ratio":{ "nodeId":"2", "fieldName":"aspect_ratio", "fieldValue":"4:3" },
"duration":{ "nodeId":"2", "fieldName":"duration", "fieldValue":5 },
"audio":{ "nodeId":"2", "fieldName":"audio", "fieldValue":true }
},
"workflowId": "2036349280088231938"
}