优化显示逻辑

This commit is contained in:
王佑琳 2026-03-10 02:32:14 +08:00
parent 6c2ac44b33
commit e6290b53e5
10 changed files with 290 additions and 179 deletions

View File

@ -2,9 +2,11 @@
<Transition name="slide-up">
<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>
<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">
@ -18,7 +20,7 @@
</div>
<Model v-model="model" />
<Proportion v-model="proportion" />
<Proportion v-model="proportion" v-model:resolution="resolution" />
<Quantity v-model="quantity" />
</div>
</template>
@ -82,9 +84,10 @@ const uploadRef = ref(null)
const model = ref('flux')
const proportion = ref('9:16')
const quantity = ref(1)
const resolution = ref('1k')
const promptPlaceholder = '描述你想生成的画面和动作。'
const prompt = ref('')
const prompt = ref('一个女孩在树下吃苹果')
const imageurl = ref('')
const imageurlShow = ref('')
const isgerenate = ref(false)
@ -164,11 +167,16 @@ const handleStart = async () => {
isgerenate.value = true
console.log('生成开始', isgerenate.value)
const data = {
videoImg: imageurl.value,
text: prompt.value,
file_type: 'video',
AIGC: 'Painting',
platform: 'runninghub',
file_type: 'image',
modelName: model.value,
prompt: prompt.value,
quantity: quantity.value,
aspect_ratio: proportion.value,
resolution: resolution.value,
}
await generate(data, 1, 1)
await generate('text', data)
console.log('生成中', isgerenate.value)
}

View File

@ -63,16 +63,25 @@ const props = defineProps({
modelValue: {
type: String,
default: '1:1'
},
resolution: {
type: String,
default: '2k'
}
})
const emit = defineEmits(['update:modelValue', 'update:width', 'update:height'])
const emit = defineEmits(['update:modelValue', 'update:resolution', 'update:width', 'update:height'])
const proportion = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
const resolution = computed({
get: () => props.resolution,
set: (value) => emit('update:resolution', value)
})
const proportionOptions = [
{ value: '智能', label: '智能' },
{ value: '21:9', label: '21:9' },
@ -91,7 +100,6 @@ const resolutionOptions = [
{ value: '4k', label: '超清 4K' }
]
const resolution = ref('2k')
const width = ref(2048)
const height = ref(2048)
@ -199,8 +207,9 @@ const getProportionStyle = (value) => {
}
}
//
updateDimensionsByResolution(resolution.value)
watch(() => [props.modelValue, props.resolution], () => {
updateDimensionsByResolution(resolution.value)
}, { immediate: true })
</script>
<style lang="less" scoped>

4
src/config/index.js Normal file
View File

@ -0,0 +1,4 @@
import * as runninghub from './runninghub/index.js'
// import * as suno from './suno.js'
export default { runninghub }

View File

@ -0,0 +1,81 @@
export function Playload(data, type) {
data = getWidthHeight(data)
console.log('宽与高', data)
if(data.modelName === 'flux'){
return flux(data)
}
}
export function result(result) {
if (result.code === 0 && result.msg === 'success') {
return { type: true, url: result.data[0].fileUrl }
}
return { type: false, message: result.data.exception_message }
}
function getWidthHeight(data) {
// 去除分辨率字符串中的'p'并转换为数字
// const resolution = 720
const resolution = Number.parseInt(data.resolution.replace('p', '')) || Number.parseInt(data.resolution)
// 解析宽高比
const aspectRatioParts = data.aspect_ratio.split(':') || data.aspect_ratio.split('')
const widthRatio = Number.parseInt(aspectRatioParts[0])
const heightRatio = Number.parseInt(aspectRatioParts[1])
if (widthRatio > heightRatio) {
data.height = resolution
data.width = Math.round(resolution * widthRatio / heightRatio)
} else {
data.width = resolution
data.height = Math.round(resolution * heightRatio / widthRatio)
}
console.log(data.width, data.height)
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' }
]
return {
workflowId: '2011689651156819970',
nodeInfoList
}
}

View File

@ -29,7 +29,7 @@ const router = createRouter({
routes
})
router.beforeEach(async (to, from, next) => {
router.beforeEach(async (to, from) => {
if(to.query.token){
setToken(to.query.token)
} else {
@ -37,7 +37,7 @@ router.beforeEach(async (to, from, next) => {
const token = getToken()
if (!token) {
// 没有 token重定向到登录页
return next('/login')
return '/login'
}
}
@ -47,7 +47,7 @@ router.beforeEach(async (to, from, next) => {
const userStore = useUserStore()
// 如果访问的是白名单路径,直接放行
if (whiteList.includes(to.path)) {
return next()
return true
}
// 检查 token 是否有效
@ -60,15 +60,15 @@ router.beforeEach(async (to, from, next) => {
// 如果用户信息不存在,则从服务器获取
await userStore.getInfo()
}
next()
return true
} else {
// token 无效,重定向到登录页
next('/login')
return '/login'
}
} catch (error) {
// 验证过程中出错,重定向到登录页
console.error('验证 token 时出错:', error)
next('/login')
return '/login'
}
})

View File

@ -1,53 +1,71 @@
const DisplayStoreSetup = () => {
const Sender_variant = ref('updown')
const scrollerRef = ref(null)
const tempList = ref([])
const scrollToBottom = () => {
console.log('store - 尝试滚动到底部')
const addGeneratingItem = (item) => {
const newItem = {
id: item.taskId || crypto.randomUUID(),
status: 'generate',
text: item.text || '生成中...',
name: item.name || '生成中...',
type: item.type || 'image',
time: item.time || new Date().toLocaleString(),
files: [],
...item
}
tempList.value.push(newItem)
return newItem
}
const updateItemToSuccess = (taskId, fileUrl) => {
const index = tempList.value.findIndex(item => item.id === taskId)
if (index !== -1) {
tempList.value[index].status = 'success'
tempList.value[index].files = [fileUrl]
}
}
const initHistoryList = (historyList) => {
tempList.value = historyList
}
const scrollToBottom = async () => {
console.log('store - 滚动到底部')
const refValue = scrollerRef.value
console.log('store - scrollerRef.value:', refValue)
if (refValue) {
console.log('store - scrollerRef.value.$el:', refValue.$el)
try {
if (typeof refValue.scrollToItem === 'function') {
console.log('store - 使用 scrollToItem 方法')
refValue.scrollToItem(refValue.items?.length - 1)
} else {
console.log('store - scrollToItem 方法不存在')
const scrollerEl = refValue.$el
if (!scrollerEl) {
console.log('store - scrollerEl 不存在')
return
}
console.log('store - scrollerEl:', scrollerEl)
const viewport = scrollerEl.querySelector('.vue-recycle-scroller__viewport')
if (viewport) {
console.log('store - 找到 viewport 元素')
viewport.scrollTop = viewport.scrollHeight
console.log('store - viewport.scrollTop:', viewport.scrollTop)
console.log('store - viewport.scrollHeight:', viewport.scrollHeight)
setTimeout(() => {
viewport.scrollTop = viewport.scrollHeight
}, 50)
}
}
} catch (error) {
console.error('store - 滚动出错:', error)
}
} else {
if (!refValue) {
console.log('store - scrollerRef 不存在')
return
}
try {
const scrollerEl = refValue.$el
if (scrollerEl) {
const viewport = scrollerEl.querySelector('.vue-recycle-scroller__viewport')
if (viewport) {
console.log('store - 原生滚动, scrollHeight:', viewport.scrollHeight)
viewport.scrollTop = viewport.scrollHeight
}
}
if (typeof refValue.scrollToItem === 'function' && tempList.value && tempList.value.length > 0) {
console.log('store - scrollToItem, index:', tempList.value.length - 1)
await nextTick()
refValue.scrollToItem(tempList.value.length - 1)
}
} catch (error) {
console.error('store - 滚动出错:', error)
}
}
return {
Sender_variant,
scrollerRef,
tempList,
addGeneratingItem,
updateItemToSuccess,
initHistoryList,
scrollToBottom
}
}

View File

@ -1,74 +1,23 @@
import { useParamStore } from '@/stores'
export async function getFormattedTime(date = new Date()) {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0') // 月份从0开始需要+1
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}`
}
import outPlatform from '@/config/index'
// 处理音频生成任务的数据并返回
export async function createTask(taskType = 1, params, title = '模特展示图') {
const paramStore = useParamStore()
const data = {
taskId: params.taskId,
taskRootId: params.taskRootId || paramStore.taskRootId,
parentTaskId: params.parentTaskId || '0',
AIGC: 'huanda',
platform: 'runninghub',
taskType,
modelName: 'Flux',
title,
file_type: params.file_type,
payload: {},
createTime: params.time,
parentCreateTime: params.parentCreateTime || '',
parentIndex: params.parentIndex || '',
token: params.token
export async function createTask(data, type, taskId, token) {
console.log(data, type)
const payload = await outPlatform[data.platform].Playload(data)
return {
AIGC: data.AIGC,
platform: data.platform,
prompt: data.prompt,
taskType: type === 'text' ? 1 : 2,
modelName: data.modelName,
payload,
taskId,
token
}
if (taskType === 1) {
data.payload = workflows.huanda
data.payload.nodeInfoList[0].fieldValue = paramStore.params.clothes
data.payload.nodeInfoList[1].fieldValue = paramStore.params.model
data.payload.nodeInfoList[2].fieldValue = paramStore.params.pose
data.payload.nodeInfoList[3].fieldValue = paramStore.params.background
data.payload.nodeInfoList[4].fieldValue = paramStore.params.model ? 0 : 1
data.payload.nodeInfoList[5].fieldValue = paramStore.params.pose ? 0 : 1
data.payload.nodeInfoList[6].fieldValue = paramStore.params.background ? 0 : 1
data.payload.nodeInfoList[7].fieldValue = params.prompt
data.payload.nodeInfoList[7].fieldValue = params.aspectRatio
} else if (taskType === 2) { // 对话修改
data.parentTaskId = params.parentTaskId
data.payload = workflows.talk
data.payload.nodeInfoList[0].fieldValue = params.text
data.payload.nodeInfoList[1].fieldValue = params.talkImg
} else if (taskType === 3) { // 生成视频
data.parentTaskId = params.parentTaskId
data.payload = workflows.video
data.payload.nodeInfoList[0].fieldValue = params.text
data.payload.nodeInfoList[1].fieldValue = params.videoImg
} else if (taskType === 4) { // AI生成模特
data.payload = workflows.model
data.payload.nodeInfoList[0].fieldValue = params.text
data.payload.nodeInfoList[1].fieldValue = params.aspectRatio
} else if (taskType === 5 || taskType === 6) { // AI生成服装背景
// data.parentTaskId = params.parentTaskId
data.payload = workflows.background_pose
data.payload.nodeInfoList[0].fieldValue = params.text
data.payload.nodeInfoList[1].fieldValue = params.aspectRatio
}
console.log('data:', data)
return data
}
// 获取音频结果
// 获取结果
export async function getTask(result) {
if (result.code === 0 && result.msg === 'success') {
return { type: true, url: result.data[0].fileUrl }

View File

@ -1,8 +1,8 @@
import { ElNotification } from 'element-plus'
import { h, ref } from 'vue'
import { useDisplayStore, useParamStore, useUserStore } from '@/stores'
import { useDisplayStore } from '@/stores'
import { getToken } from '@/utils/auth'
import { createTask, getFormattedTime, getTask } from '@/utils/createTask'
import { createTask, getTask } from '@/utils/createTask'
import { userError } from '@/utils/tokenError'
export function websocketError(code, msg) {
@ -47,14 +47,17 @@ export function websocketSuccess() {
})
}
export async function generate(taskType, data, type) {
export async function generate(type, data) {
const progress_text = ref('')
const message = ref('')
const previewUrl = ref('')
const useDisplay = useDisplayStore()
const token = getToken()
const taskId = crypto.randomUUID()
let currentTaskId = null
const result = await createTask(taskType, { text: data.prompt, aspect_ratio: data.aspectRatio, token })
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 socket = new WebSocket(wsURL)
@ -73,7 +76,7 @@ export async function generate(taskType, data, type) {
return
} else if (event.data === 'please give me taskId') {
socket.send(`setTaskId:${taskId}`)
progerss_text.value = '信息提交中...'
progress_text.value = '信息提交中...'
return
} else if (event.data === 'OK! Please continue. ') {
socket.send(JSON.stringify({
@ -82,12 +85,16 @@ export async function generate(taskType, data, type) {
}))
return
} else if (event.data === '任务提交成功,正在排队中...') {
progerss_text.value = '视频生成中...'
// 启动进度条更新
startTime.value = Date.now()
progressPercent.value = 0
progressInterval.value = window.setInterval(updateProgress, 1000)
progress.value = true
progress_text.value = '视频生成中...'
currentTaskId = taskId
useDisplay.addGeneratingItem({
taskId: taskId,
text: data.prompt || '生成中...',
name: data.prompt || '生成中...',
type: type
})
return
}
message.value = event.data
@ -114,7 +121,6 @@ export async function generate(taskType, data, type) {
// 处理链接关闭
socket.onclose = async (event) => {
console.log('WebSocket已关闭:', event)
isGenerating.value = false
// 清理心跳定时器
if (heartbeatInterval) {
@ -126,9 +132,14 @@ export async function generate(taskType, data, type) {
userError()
} else if (event.code === 1000 && event.reason === 'success') {
console.log('收到服务器消息:', res)
const result = await getVideo(tempPlatform.value, res)
const result = await getTask(res)
if (result.type) {
previewUrl.value = result.url
if (currentTaskId) {
useDisplay.updateItemToSuccess(currentTaskId, result.url)
}
websocketSuccess()
} else {
websocketError(4403, res.message)
@ -136,10 +147,7 @@ export async function generate(taskType, data, type) {
} else {
websocketError(event.code, event.reason)
}
progress.value = false
clearInterval(progressInterval.value)
progressInterval.value = null
isGenerating.value = false
// clearInterval(progressInterval.value)
// 清理心跳定时器
if (heartbeatInterval) {
clearInterval(heartbeatInterval)

View File

@ -27,7 +27,7 @@ defineProps({
left: 0;
width: 100vw;
height: 100vh;
background-color: rgba(255, 255, 255, 0.9);
background-color: rgb(255, 255, 255);
display: flex;
align-items: center;
justify-content: center;

View File

@ -66,6 +66,7 @@
<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'
@ -91,6 +92,7 @@ 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 = [
@ -107,27 +109,30 @@ const favoriteOptions = [
const selectedTime = ref('all')
const selectedFavorite = ref('all')
const tempList = ref([])
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 list = ref()
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)
list.value = tempList.value
//
const toggleDisplay = (newValue, oldValue) => {
if ((newValue === 'image' || newValue === 'video') && oldValue === 'all') {
list.value = tempList.value.filter((item) => item.type === newValue)
} else if ((newValue === 'image' || newValue === 'video') && (oldValue === 'image' || oldValue === 'video')) {
list.value = tempList.value.filter((item) => item.type === newValue)
} else {
list.value = tempList.value
}
console.log(list.value)
activeFilter.value = newValue
}
//
@ -149,8 +154,12 @@ const conversion = (newlist) => {
//
const fetchHistory= async (isScrollTopLoad = false) => {
try {
if (isScrollTopLoad) {
return
}
const result = await getGenerateHistoryList({ userId: userStore.userInfo.id, chargeType: 1 })
total = result.data.length
total = result.data ? result.data.length : 0
if (total === 0) {
useDisplay.Sender_variant = 'updown'
router.push({ name: 'home' })
@ -175,19 +184,48 @@ const fetchHistory= async (isScrollTopLoad = false) => {
})
if (!isScrollTopLoad && adaptedList.length > 0) {
tempList.value = [adaptedList[adaptedList.length - 1]]
list.value = tempList.value
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(() => {
const remainingItems = adaptedList.slice(0, -1)
tempList.value = [...remainingItems, ...tempList.value]
list.value = tempList.value
console.log('添加剩余数据后列表长度:', tempList.value.length)
}, 1000)
await nextTick()
scrollToBottomDirect(true)
setTimeout(() => {
refreshing.value = false
isInitializing.value = false
useDisplay.scrollToBottom()
}, 600)
}, 1500)
} else {
tempList.value = adaptedList
list.value = tempList.value
useDisplay.initHistoryList(adaptedList)
}
} catch (error) {
console.error('获取历史失败:', error)
@ -201,25 +239,28 @@ const fetchHistory= async (isScrollTopLoad = false) => {
//
const getList = async () => {
if (isLoadingMore.value) return
isLoadingMore.value = true
await fetchHistory(true)
isLoadingMore.value = false
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 (scrollTop <= 50 && !isLoadingMore.value) {
// getList()
// }
if (distanceToBottom <= 50) {
useDisplay.Sender_variant = 'updown'
refreshing.value = false
} else if (distanceToBottom >= 350) {
useDisplay.Sender_variant = 'default'
}
@ -229,21 +270,14 @@ onMounted(() => {
console.log('display 组件已挂载')
if (!props.loading) return
refreshing.value = true
fetchHistory()
nextTick(() => {
console.log('设置 scrollerRef 到 store')
useDisplay.scrollerRef = scrollerRef.value
useDisplay.scrollToBottom()
fetchHistory()
})
setTimeout(() => {
console.log('setTimeout 后尝试滚动')
useDisplay.scrollToBottom()
setTimeout(() => {
refreshing.value = false
}, 1000)
}, 1000)
page.value++
})
</script>