优化set组件 里的prompt的显示逻辑

This commit is contained in:
王佑琳 2026-03-30 19:14:25 +08:00
parent ff4ae2bdc8
commit 727ecb378b
9 changed files with 710 additions and 401 deletions

2
components.d.ts vendored
View File

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

View File

@ -83,7 +83,6 @@ const isgerenate = ref(false)
const model = ref('flux') const model = ref('flux')
const modelType = ref('text') const modelType = ref('text')
//
const prompt = ref('一个女孩在树下吃苹果') const prompt = ref('一个女孩在树下吃苹果')
const promptPlaceholder = '描述你想生成的画面和动作。' const promptPlaceholder = '描述你想生成的画面和动作。'
const proportion = ref('16:9') const proportion = ref('16:9')
@ -106,8 +105,10 @@ const autoSizeConfig = computed(() => {
}) })
const handleStart = async () => { const handleStart = async () => {
const currentType = props.type
if (!props.isGenerate) { if (!props.isGenerate) {
router.push({ name: 'home', query: { loading: false, Generate: true } }) router.push({ name: 'home', query: { loading: false, Generate: true, type: currentType } })
} }
if (!prompt.value) { if (!prompt.value) {
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
@ -122,8 +123,7 @@ const handleStart = async () => {
}) })
console.log('imgs', imgs) console.log('imgs', imgs)
const result = { const generateData = {
type: props.type,
model: model.value, model: model.value,
modelType: modelType.value, modelType: modelType.value,
prompt: prompt.value, prompt: prompt.value,
@ -147,15 +147,15 @@ const handleStart = async () => {
{ name: 'resolution', data: resolution.value}, { name: 'resolution', data: resolution.value},
], ],
imgs, imgs,
result result: JSON.stringify(generateData)
} }
await generate(modelType.value, data) await generate(modelType.value, data, generateData, props.type)
console.log('生成中', isgerenate.value) console.log('生成中', isgerenate.value)
} }
const fillParamsFromResult = (resultData) => { const fillParamsFromResult = (resultData) => {
if (!resultData) return if (!resultData) return
if (resultData.model !== undefined) model.value = resultData.model if (resultData.model !== undefined) model.value = resultData.model
if (resultData.modelType !== undefined) modelType.value = resultData.modelType if (resultData.modelType !== undefined) modelType.value = resultData.modelType
if (resultData.prompt !== undefined) prompt.value = resultData.prompt if (resultData.prompt !== undefined) prompt.value = resultData.prompt
@ -196,6 +196,15 @@ watch(() => useDisplay.isSubGerenate, (newValue) => {
watch(() => modelType.value, (newValue) => { watch(() => modelType.value, (newValue) => {
console.log('modelType.value', newValue) console.log('modelType.value', newValue)
}) })
watch(() => props.type, (newType) => {
if (newType === 'video') {
model.value = 'Vidu Q3-T2V'
} else {
model.value = 'flux'
}
})
onMounted(() => { onMounted(() => {
if(props.type === 'video'){ if(props.type === 'video'){
model.value = 'Vidu Q3-T2V' model.value = 'Vidu Q3-T2V'

View File

@ -1,379 +1,613 @@
<template> <template>
<div class="virtual-scroller" :style="containerStyle"> <div
ref="containerRef"
class="virtual-scroller"
:style="containerStyle"
>
<div <div
ref="scrollContainerRef" ref="wrapperRef"
class="virtual-scroller-container" class="virtual-scroller-wrapper"
:style="scrollContainerStyle" :style="wrapperStyle"
@scroll.passive="handleScroll"
> >
<div class="virtual-scroller-spacer" :style="spacerStyle"></div>
<div class="virtual-scroller-placeholder" :style="placeholderStyle">
<slot name="bottom-placeholder" />
</div>
<div <div
v-for="item in visibleItems" ref="renderContainerRef"
:key="item.key" class="virtual-scroller-render-container"
:ref="el => setItemRef(el, item.key)" :style="renderContainerStyle"
class="virtual-scroller-item" @scroll.passive="handleScroll"
:style="getItemStyle(item)" @wheel="handleWheel"
:data-index="item.index"
:data-key="item.key"
> >
<slot name="default" :item="item.data" :index="item.index" /> <div class="virtual-scroller-spacer" :style="{ height: `${totalHeight}px` }"></div>
<div
class="virtual-scroller-bottom-placeholder"
:style="bottomPlaceholderStyle"
>
<slot name="bottom-placeholder" />
</div>
<div
v-for="renderItem in visibleItems"
:key="getItemKey(renderItem.item, renderItem.index)"
:ref="el => setItemRef(el, renderItem.index)"
class="virtual-scroller-item"
:style="getItemStyle(renderItem)"
:data-index="renderItem.index"
>
<slot
name="default"
:item="renderItem.item"
:index="renderItem.index"
/>
</div>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup>
import { ref, computed, watch, onMounted, onUnmounted, nextTick, type CSSProperties } from 'vue' import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
interface VirtualScrollerProps { const props = defineProps({
items: any[] data: {
estimatedHeight?: number type: Array,
bufferSize?: number required: false,
keyField?: string default: () => []
height?: string | number },
direction?: 'normal' | 'reverse' items: {
} type: Array,
required: false,
const props = withDefaults(defineProps<VirtualScrollerProps>(), { default: () => []
estimatedHeight: 100, },
bufferSize: 3, itemKey: {
keyField: 'id', type: [String, Function],
height: '100%', default: 'id'
direction: 'reverse' },
keyField: {
type: String,
default: 'id'
},
estimatedHeight: {
type: Number,
default: 100
},
buffer: {
type: Number,
default: 3
},
bufferSize: {
type: Number,
default: 3
},
height: {
type: [String, Number],
default: '100%'
},
width: {
type: [String, Number],
default: '100%'
},
renderMode: {
type: String,
default: 'default',
validator: (value) => ['default', 'top'].includes(value)
},
direction: {
type: String,
default: 'reverse',
validator: (value) => ['normal', 'reverse'].includes(value)
},
bottomPlaceholderHeight: {
type: Number,
default: 350
}
}) })
const emit = defineEmits<{ const computedData = computed(() => {
scroll: [scrollTop: number, scrollInfo: { return props.data.length > 0 ? props.data : props.items
isAtTop: boolean })
isAtBottom: boolean
distanceToTop: number
distanceToBottom: number
}]
'visible-change': [startIndex: number, endIndex: number]
}>()
const scrollContainerRef = ref<HTMLElement | null>(null) const computedItemKey = computed(() => {
if (typeof props.itemKey === 'function') return props.itemKey
if (props.itemKey !== 'id') return props.itemKey
return props.keyField
})
const computedBuffer = computed(() => {
return props.buffer !== 3 ? props.buffer : props.bufferSize
})
const emit = defineEmits(['scroll', 'scroll-start', 'scroll-end', 'visible-change'])
const containerRef = ref(null)
const wrapperRef = ref(null)
const renderContainerRef = ref(null)
const itemRefs = new Map()
const itemHeights = ref(new Map())
const resizeObserver = ref(null)
const scrollTop = ref(0) const scrollTop = ref(0)
const itemRefs = new Map<string, HTMLElement>() const isScrolling = ref(false)
const isReverseMode = computed(() => props.direction === 'reverse') const scrollTimeout = ref(null)
const isInitializing = ref(true) const isInitialized = ref(false)
const pendingScrollToBottom = ref(false)
const previousDataLength = ref(0)
const itemHeights = ref(new Map<string | number, number>()) const containerStyle = computed(() => {
const itemOffsets = ref(new Map<string | number, number>()) return {
let resizeObserver: ResizeObserver | null = null height: '100%',
width: '100%',
function getItemKey(item: any, index: number): string | number { position: 'relative'
return item[props.keyField] ?? index
}
function getItemHeight(key: string | number): number {
return itemHeights.value.get(key) ?? props.estimatedHeight
}
function calculateOffsets() {
let offset = 0
const newOffsets = new Map<string | number, number>()
for (let i = 0; i < props.items.length; i++) {
const key = getItemKey(props.items[i], i)
newOffsets.set(key, offset)
offset += getItemHeight(key)
} }
})
itemOffsets.value = newOffsets
}
const totalHeight = computed(() => { const totalHeight = computed(() => {
let height = 0 let height = 0
for (let i = 0; i < props.items.length; i++) { const len = computedData.value.length
const key = getItemKey(props.items[i], i)
height += getItemHeight(key) for (let i = 0; i < len; i++) {
const cachedHeight = itemHeights.value.get(i)
height += cachedHeight !== undefined ? cachedHeight : props.estimatedHeight
} }
height += props.bottomPlaceholderHeight
return height return height
}) })
const containerStyle = computed<CSSProperties>(() => ({ const getItemPosition = (index) => {
height: typeof props.height === 'number' ? `${props.height}px` : props.height, let offset = 0
overflow: 'hidden'
})) for (let i = 0; i < index; i++) {
const cachedHeight = itemHeights.value.get(i)
offset += cachedHeight !== undefined ? cachedHeight : props.estimatedHeight
}
const height = itemHeights.value.get(index) ?? props.estimatedHeight
return { offset, height }
}
const scrollContainerStyle = computed<CSSProperties>(() => ({ const containerHeight = computed(() => {
height: '100%', if (!renderContainerRef.value) return 0
overflowY: 'auto', return renderContainerRef.value.clientHeight
overflowX: 'hidden', })
transform: 'rotate(180deg)',
position: 'relative'
}))
const spacerStyle = computed<CSSProperties>(() => ({
height: `${totalHeight.value}px`,
width: '1px',
pointerEvents: 'none',
position: 'absolute',
top: 0,
left: 0
}))
const placeholderStyle = computed<CSSProperties>(() => ({
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
pointerEvents: 'none'
}))
const visibleRange = computed(() => { const visibleRange = computed(() => {
if (!scrollContainerRef.value || props.items.length === 0) { if (!renderContainerRef.value || computedData.value.length === 0) {
return { start: 0, end: Math.min(props.bufferSize * 2, props.items.length - 1) } return { start: 0, end: 0, offset: 0 }
} }
const containerHeight = scrollContainerRef.value.clientHeight const viewportHeight = containerHeight.value
const scrollTopValue = scrollTop.value const currentScrollTop = scrollTop.value
const bufferCount = computedBuffer.value
let startIndex = 0 let startIndex = 0
let endIndex = props.items.length - 1 let endIndex = computedData.value.length - 1
let currentOffset = 0 let startOffset = 0
for (let i = 0; i < props.items.length; i++) { let offset = 0
const key = getItemKey(props.items[i], i) for (let i = 0; i < computedData.value.length; i++) {
const itemHeight = getItemHeight(key) const cachedHeight = itemHeights.value.get(i)
const height = cachedHeight !== undefined ? cachedHeight : props.estimatedHeight
if (currentOffset + itemHeight > scrollTopValue - props.bufferSize * props.estimatedHeight) { if (offset + height > currentScrollTop) {
startIndex = Math.max(0, i - props.bufferSize) startIndex = Math.max(0, i - bufferCount)
break break
} }
currentOffset += itemHeight
offset += height
} }
currentOffset = 0 startOffset = 0
for (let i = 0; i < props.items.length; i++) { for (let i = 0; i < startIndex; i++) {
const key = getItemKey(props.items[i], i) const cachedHeight = itemHeights.value.get(i)
const itemHeight = getItemHeight(key) startOffset += cachedHeight !== undefined ? cachedHeight : props.estimatedHeight
}
offset = startOffset
for (let i = startIndex; i < computedData.value.length; i++) {
const cachedHeight = itemHeights.value.get(i)
const height = cachedHeight !== undefined ? cachedHeight : props.estimatedHeight
if (currentOffset > scrollTopValue + containerHeight + props.bufferSize * props.estimatedHeight) { if (offset >= currentScrollTop + viewportHeight) {
endIndex = Math.min(props.items.length - 1, i + props.bufferSize) endIndex = Math.min(computedData.value.length - 1, i + bufferCount)
break break
} }
currentOffset += itemHeight
offset += height
endIndex = i
} }
return { start: startIndex, end: endIndex } return { start: startIndex, end: endIndex, offset: startOffset }
}) })
const visibleItems = computed(() => { const visibleItems = computed(() => {
const { start, end } = visibleRange.value const { start, end, offset } = visibleRange.value
const items: Array<{ key: string | number; data: any; index: number; offset: number }> = [] const items = []
const currentRenderedKeys = new Set<string | number>()
let currentOffset = offset
for (let i = start; i <= end; i++) {
const item = props.items[i] for (let i = start; i <= end && i < computedData.value.length; i++) {
if (!item) continue const cachedHeight = itemHeights.value.get(i)
const height = cachedHeight !== undefined ? cachedHeight : props.estimatedHeight
const key = getItemKey(item, i)
if (currentRenderedKeys.has(key)) {
continue
}
currentRenderedKeys.add(key)
const offset = itemOffsets.value.get(key) ?? i * props.estimatedHeight
items.push({ items.push({
key: String(key), item: computedData.value[i],
data: item,
index: i, index: i,
offset offset: currentOffset + props.bottomPlaceholderHeight,
height
}) })
currentOffset += height
} }
return items return items
}) })
function getItemStyle(item: { offset: number }): CSSProperties { const wrapperStyle = computed(() => ({
direction: 'rtl',
height: '100%',
position: 'relative',
scrollbarWidth: 'auto',
overflow: 'hidden',
transform: 'rotate(180deg)',
width: '100%'
}))
const renderContainerStyle = computed(() => ({
direction: 'ltr',
display: 'flex',
flexDirection: 'column',
justifyContent: 'flex-end',
bottom: 0,
left: 0,
overflowX: 'hidden',
overflowY: 'auto',
position: 'absolute',
right: 0,
top: 0,
width: '100%'
}))
const bottomPlaceholderStyle = computed(() => ({
position: 'absolute',
left: 0,
right: 0,
top: 0,
width: '100%',
height: `${props.bottomPlaceholderHeight}px`,
transform: `translateY(0px)`,
zIndex: 1
}))
const getItemKey = (item, index) => {
const keyField = computedItemKey.value
if (typeof keyField === 'function') {
return keyField(item, index)
}
if (typeof keyField === 'string' && item && typeof item === 'object') {
return item[keyField] ?? index
}
return index
}
const getItemStyle = (renderItem) => {
return { return {
position: 'absolute', position: 'absolute',
top: `${item.offset}px`,
left: 0, left: 0,
right: 0 right: 0,
top: 0,
width: '100%',
transform: `translateY(${renderItem.offset}px)`,
willChange: 'transform'
} }
} }
function setItemRef(el: any, key: string) { const setItemRef = (el, index) => {
if (el) { if (el) {
itemRefs.set(key, el as HTMLElement) itemRefs.set(index, el)
} else { } else {
itemRefs.delete(key) itemRefs.delete(index)
} }
} }
function measureItems() { const measureItem = (index, element) => {
if (!resizeObserver) return if (!element) return
itemRefs.forEach((el, key) => { const firstChild = element.firstElementChild
resizeObserver!.observe(el) const targetElement = firstChild || element
})
}
function updateItemHeight(key: string | number, height: number) {
const oldHeight = itemHeights.value.get(key)
if (oldHeight !== height) {
itemHeights.value.set(key, height)
calculateOffsets()
}
}
function handleScroll(event: Event) {
const target = event.target as HTMLElement
const currentScrollTop = target.scrollTop
scrollTop.value = currentScrollTop
if (!scrollContainerRef.value) { const height = targetElement.getBoundingClientRect().height
emit('scroll', currentScrollTop)
return
}
const containerHeight = scrollContainerRef.value.clientHeight if (height > 0) {
const maxScroll = Math.max(0, totalHeight.value - containerHeight) const cachedHeight = itemHeights.value.get(index)
if (cachedHeight !== height) {
const distanceToTop = currentScrollTop const newHeights = new Map(itemHeights.value)
const distanceToBottom = maxScroll - currentScrollTop newHeights.set(index, height)
itemHeights.value = newHeights
const isAtTop = currentScrollTop >= maxScroll - 10
const isAtBottom = currentScrollTop <= 10
emit('scroll', currentScrollTop, {
isAtTop,
isAtBottom,
distanceToTop,
distanceToBottom
})
}
function scrollToIndex(index: number) {
if (!scrollContainerRef.value) return
let targetTop = 0
for (let i = 0; i < index; i++) {
const key = getItemKey(props.items[i], i)
targetTop += getItemHeight(key)
}
scrollContainerRef.value.scrollTop = targetTop
scrollTop.value = targetTop
}
function scrollToTop() {
if (!scrollContainerRef.value) return
const containerHeight = scrollContainerRef.value.clientHeight
const maxScroll = Math.max(0, totalHeight.value - containerHeight)
scrollContainerRef.value.scrollTop = maxScroll
scrollTop.value = maxScroll
}
function scrollToBottom() {
if (!scrollContainerRef.value) return
scrollContainerRef.value.scrollTop = 0
scrollTop.value = 0
}
watch(
() => visibleRange.value,
(newRange) => {
emit('visible-change', newRange.start, newRange.end)
nextTick(() => {
measureItems()
})
},
{ deep: true }
)
watch(
() => props.items,
() => {
calculateOffsets()
nextTick(() => {
measureItems()
})
},
{ immediate: true }
)
watch(
() => props.items.length,
async (newLength, oldLength) => {
if (isReverseMode.value && newLength > (oldLength || 0)) {
await nextTick()
scrollToBottom()
} }
} }
) }
onMounted(() => { const setupResizeObserver = () => {
resizeObserver = new ResizeObserver((entries) => { if (resizeObserver.value) {
resizeObserver.value.disconnect()
}
resizeObserver.value = new ResizeObserver((entries) => {
for (const entry of entries) { for (const entry of entries) {
const el = entry.target as HTMLElement const index = parseInt(entry.target.dataset.index, 10)
const key = el.dataset.key if (!isNaN(index)) {
if (key) { measureItem(index, entry.target)
updateItemHeight(key, entry.contentRect.height)
} }
} }
}) })
}
const handleWheel = (event) => {
if (!renderContainerRef.value) return
if (scrollContainerRef.value && isReverseMode.value) { const { deltaY } = event
nextTick(() => { const el = renderContainerRef.value
scrollToBottom()
setTimeout(() => { el.scrollBy({
isInitializing.value = false top: -deltaY,
}, 100) behavior: 'instant'
}) })
} else {
isInitializing.value = false event.preventDefault()
}
const handleScroll = (event) => {
const target = event.target
scrollTop.value = target.scrollTop
isScrolling.value = true
if (scrollTimeout.value) {
clearTimeout(scrollTimeout.value)
} }
measureItems() scrollTimeout.value = setTimeout(() => {
isScrolling.value = false
}, 150)
const st = target.scrollTop
const scrollHeight = target.scrollHeight
const clientHeight = target.clientHeight
const distanceToContainerTop = st
const distanceToContainerBottom = scrollHeight - st - clientHeight
const distanceToPageTop = distanceToContainerBottom
const distanceToPageBottom = distanceToContainerTop
const isAtPageTop = distanceToPageTop <= 0
const isAtPageBottom = distanceToPageBottom <= 0
emit('scroll', {
target,
scrollTop: st,
scrollHeight,
clientHeight,
distanceToPageTop,
distanceToPageBottom,
isAtPageTop,
isAtPageBottom
})
if (isAtPageTop) {
emit('scroll-start')
}
if (isAtPageBottom) {
emit('scroll-end')
}
}
const scrollToIndex = (index, behavior = 'auto') => {
if (!renderContainerRef.value || index < 0 || index >= computedData.value.length) return
const position = getItemPosition(index)
renderContainerRef.value.scrollTo({
top: position.offset,
behavior
})
}
const scrollToBottom = (behavior = 'smooth') => {
if (!renderContainerRef.value) {
pendingScrollToBottom.value = true
return
}
requestAnimationFrame(() => {
if (!renderContainerRef.value) return
renderContainerRef.value.scrollTo({
top: 0,
behavior
})
})
}
const scrollToTop = (behavior = 'smooth') => {
if (!renderContainerRef.value) return
requestAnimationFrame(() => {
if (!renderContainerRef.value) return
const scrollHeight = renderContainerRef.value.scrollHeight
renderContainerRef.value.scrollTo({
top: scrollHeight,
behavior
})
})
}
const getScrollElement = () => renderContainerRef.value
const getVisibleIndices = () => {
const { start, end } = visibleRange.value
return Array.from({ length: end - start + 1 }, (_, i) => start + i)
}
const resetMeasurements = () => {
itemHeights.value = new Map()
itemRefs.clear()
}
const isAtPageBottom = () => {
if (!renderContainerRef.value) return false
const { scrollTop } = renderContainerRef.value
return scrollTop <= 0
}
const isAtPageTop = () => {
if (!renderContainerRef.value) return false
const { scrollTop, scrollHeight, clientHeight } = renderContainerRef.value
return scrollHeight - scrollTop - clientHeight <= 0
}
const observeVisibleItems = () => {
if (!resizeObserver.value) return
resizeObserver.value.disconnect()
for (const [index, element] of itemRefs) {
if (element) {
resizeObserver.value.observe(element)
}
}
}
watch(() => computedData.value, (newData, oldData) => {
const oldLength = oldData?.length || 0
const newLength = newData.length
if (newLength !== oldLength) {
const newHeights = new Map()
const minLen = Math.min(oldLength, newLength)
for (let i = 0; i < minLen; i++) {
if (itemHeights.value.has(i)) {
newHeights.set(i, itemHeights.value.get(i))
}
}
itemHeights.value = newHeights
nextTick(() => {
observeVisibleItems()
})
}
previousDataLength.value = newLength
}, { deep: false })
watch(visibleItems, (newItems) => {
nextTick(() => {
observeVisibleItems()
})
if (newItems.length > 0) {
const firstItem = newItems[0]
const lastItem = newItems[newItems.length - 1]
emit('visible-change', firstItem.index, lastItem.index)
}
}, { deep: true })
onMounted(() => {
setupResizeObserver()
isInitialized.value = true
previousDataLength.value = computedData.value.length
nextTick(() => {
if (pendingScrollToBottom.value) {
pendingScrollToBottom.value = false
scrollToBottom()
}
observeVisibleItems()
})
}) })
onUnmounted(() => { onBeforeUnmount(() => {
if (resizeObserver) { if (resizeObserver.value) {
resizeObserver.disconnect() resizeObserver.value.disconnect()
resizeObserver = null
} }
if (scrollTimeout.value) {
clearTimeout(scrollTimeout.value)
}
itemRefs.clear() itemRefs.clear()
}) })
defineExpose({ defineExpose({
scrollToIndex, scrollToIndex,
scrollToTop, scrollToItem: scrollToIndex,
scrollToBottom, scrollToBottom,
getScrollTop: () => scrollTop.value, scrollToTop,
getVisibleRange: () => visibleRange.value, getScrollElement,
updateLayout: () => { getVisibleIndices,
calculateOffsets() resetMeasurements,
measureItems() containerRef,
} isAtPageBottom,
isAtPageTop
}) })
</script> </script>
<style scoped> <style lang="less" scoped>
.virtual-scroller { .virtual-scroller {
position: relative; -webkit-overflow-scrolling: touch;
width: 100%;
} &::-webkit-scrollbar {
display: none;
.virtual-scroller-container { width: 6px;
position: relative; height: 6px;
width: 100%; }
}
&::-webkit-scrollbar-track {
.virtual-scroller-spacer { background: transparent;
flex-shrink: 0; }
}
&::-webkit-scrollbar-thumb {
.virtual-scroller-item { background: rgba(0, 0, 0, 0.2);
will-change: transform; border-radius: 3px;
contain: layout style;
&:hover {
background: rgba(0, 0, 0, 0.3);
}
}
.virtual-scroller-wrapper {
contain: content;
}
.virtual-scroller-spacer {
flex-shrink: 0;
width: 100%;
}
.virtual-scroller-placeholder {
width: 100%;
}
.virtual-scroller-render-container {
contain: layout style;
&::-webkit-scrollbar {
display: none;
width: 6px;
height: 6px;
}
}
.virtual-scroller-item {
contain: layout style;
backface-visibility: hidden;
perspective: 1000px;
}
.virtual-scroller-bottom-placeholder {
contain: layout style;
}
} }
</style> </style>

View File

@ -18,12 +18,10 @@ const DisplayStoreSetup = () => {
const newItem = { const newItem = {
id: item.taskId || crypto.randomUUID(), id: item.taskId || crypto.randomUUID(),
status: 'generate', status: 'generate',
text: item.text || '生成中...',
name: item.name || '生成中...',
type: item.type || 'image', type: item.type || 'image',
time: item.time || new Date().toLocaleString(), time: item.time || new Date().toLocaleString(),
files: [], files: [],
...item generateData: item.generateData || {}
} }
tempList.value.unshift(newItem) tempList.value.unshift(newItem)
return newItem return newItem

View File

@ -13,7 +13,8 @@ export async function createTask(data, type, taskId, token) {
modelName: data.modelName, modelName: data.modelName,
payload, payload,
taskId, taskId,
token token,
result: data.result
} }
} }

View File

@ -10,7 +10,7 @@ export function getChargeType(chargeType) {
case 'painting': case 'painting':
return 1 return 1
case 'video': case 'video':
return 2 return 4
default: default:
return 2 return 2
} }
@ -58,7 +58,7 @@ export function websocketSuccess() {
}) })
} }
export async function generate(type, data) { export async function generate(modelType, data, generateData, type) {
const progress_text = ref('') const progress_text = ref('')
const message = ref('') const message = ref('')
const useDisplay = useDisplayStore() const useDisplay = useDisplayStore()
@ -68,7 +68,7 @@ export async function generate(type, data) {
useDisplay.isSubGerenate = true useDisplay.isSubGerenate = true
const result = await createTask(data, type, taskId, token) const result = await createTask(data, modelType, taskId, token)
console.log(result) console.log(result)
// const token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpblR5cGUiOiJsb2dpbiIsImxvZ2luSWQiOjY0NDEwODAyMjk1OTgzNzIzMCwicm5TdHIiOiJiWkVwS2JLWFJyZmRIaFFHWXZKTkdzOGdGM0JSRmxQOCJ9.5eQ2GtVdrDntQDe2tnF8vl_DhTfd2uW-KNqzvl1imc0' // const token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpblR5cGUiOiJsb2dpbiIsImxvZ2luSWQiOjY0NDEwODAyMjk1OTgzNzIzMCwicm5TdHIiOiJiWkVwS2JLWFJyZmRIaFFHWXZKTkdzOGdGM0JSRmxQOCJ9.5eQ2GtVdrDntQDe2tnF8vl_DhTfd2uW-KNqzvl1imc0'
const wsURL = `${import.meta.env.VITE_API_WORKFLOW_WS}/?token=Bearer ${token}` const wsURL = `${import.meta.env.VITE_API_WORKFLOW_WS}/?token=Bearer ${token}`
@ -102,10 +102,8 @@ export async function generate(type, data) {
useDisplay.addGeneratingItem({ useDisplay.addGeneratingItem({
taskId: taskId, taskId: taskId,
text: data.prompt || '生成中...',
name: data.prompt || '生成中...',
type: type, type: type,
result: data.result generateData: generateData
}) })
setTimeout(() => { setTimeout(() => {
useDisplay.scrollToBottom() useDisplay.scrollToBottom()

View File

@ -2,25 +2,14 @@
<div style="width: 100%;display: flex;justify-content: center;align-items: center;transform: rotate(180deg);"> <div style="width: 100%;display: flex;justify-content: center;align-items: center;transform: rotate(180deg);">
<div class="primary-box" :class="{ 'none-primary-box': props.item.status === 'none' }"> <div class="primary-box" :class="{ 'none-primary-box': props.item.status === 'none' }">
<!-- 标题 --> <!-- 标题 -->
<div class="title"> <div class="title" ref="titleRef" @mouseenter="isHovering = true" @mouseleave="isHovering = false">
<div class="style"> <div class="prompt-wrapper">
<div class="name" ref="nameRef">{{ props.item.text }}</div> <div class="prompt" ref="nameRef" :class="{ 'expanded': isHovering }">{{ props.item.generateData.prompt || '生成图片' }}</div>
<div class="generate-data" ref="generateDataRef"> <div class="generate-data" v-show="!isHovering" ref="generateDa taRef" :class="{ 'second-line': shouldShowOnSecondLine }">
<div class="detailed-data first-detailed-data">{{ props.item.model }}</div> <div class="detailed-data first-detailed-data">{{ props.item.generateData.model }}</div>
<!-- <div class="detailed-data">多少秒</div> --> <div class="detailed-data">{{ props.item.generateData.proportion }}</div>
<div class="detailed-data">{{ props.item.proportion }}</div>
<div class="detailed-data">{{ props.item.resolution }}</div>
<div class="detailed-data">{{ props.item.quantity }}</div>
<!-- <div class="detailed-data">多少k</div> -->
</div> </div>
</div> </div>
<div v-if="props.item.parentIndex" class="style">
<span class="time">源自 {{ props.item.parentTime }}</span>
<div class="dividing-line"></div>
<span class="time">源自 {{ props.item.parentTime }}</span>
<div v-if="props.item.parentIndex" class="dividing-line"></div>
<span class="time"> {{ props.item.parentIndex }} 张图片</span>
</div>
</div> </div>
<!-- 加载中 --> <!-- 加载中 -->
@ -49,8 +38,8 @@
</div> </div>
</div> </div>
<!-- 已完成 --> <!-- 已完成 图片 -->
<div v-if="props.item.status === 'success'" class="box success-box"> <div v-if="props.item.status === 'success' && props.item.type === 'painting'" class="box success-box">
<div v-for="(file, index) in props.item.files" :key="index" class="one-box" :class="{ 'collected': isCollected(file) }" @mouseenter="hoverIndex = index" @mouseleave="hoverIndex = -1"> <div v-for="(file, index) in props.item.files" :key="index" class="one-box" :class="{ 'collected': isCollected(file) }" @mouseenter="hoverIndex = index" @mouseleave="hoverIndex = -1">
<!-- <img :src="file" alt="index" class="img" /> --> <!-- <img :src="file" alt="index" class="img" /> -->
<Img :src="file" alt="index" class="img" /> <Img :src="file" alt="index" class="img" />
@ -74,7 +63,21 @@
</div> </div>
</div> </div>
<div v-if="props.item.status === 'success'" class="bottom-btn-group"> <!-- 已完成 视频 -->
<div v-if="props.item.status === 'success' && props.item.type === 'video'" class="box success-box">
<div class="one-box" :class="{ 'collected': isCollected(props.item.files[0]) }" @mouseenter="hoverIndex = 0" @mouseleave="hoverIndex = -1">
<!-- <img :src="file" alt="index" class="img" /> -->
<video :src="props.item.files[0]" alt="index" class="video" />
<div class="left-top">
<div v-show="hoverIndex === 0" class="left-top-btn download-btn" @click="downloadImage(props.item.files[0], 'video')"><img src="@/assets/display/download.svg" /></div>
<span v-if="hoverIndex === 0" class="line" />
<div class="left-top-btn collect-btn" @click="addCollection(props.item.files[0])"><img :src="isCollected(props.item.files[0]) ? collectionActiveIcon : collectionIcon" /></div>
</div>
</div>
</div>
<div v-if="props.item.status === 'success'" class="bottom-btn-group" style="margin-top: 8px;">
<div v-for="(item, index) in bottomBtnGroup" :key="index" class="bottom-btn" @click="item.click()"> <div v-for="(item, index) in bottomBtnGroup" :key="index" class="bottom-btn" @click="item.click()">
<img :src="item.icon" /> <img :src="item.icon" />
<span>{{ item.name }}</span> <span>{{ item.name }}</span>
@ -102,7 +105,6 @@ const props = defineProps({
default: () => ({}) default: () => ({})
} }
}) })
const emit = defineEmits(['open-canvas', 'delete-success']) const emit = defineEmits(['open-canvas', 'delete-success'])
const useDisplay = useDisplayStore() const useDisplay = useDisplayStore()
@ -111,6 +113,76 @@ const useUser = useUserStore()
const localCollectStatus = ref({ ...props.item.collectStatus }) const localCollectStatus = ref({ ...props.item.collectStatus })
const hoverIndex = ref(-1) 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 checkTextOverflow = () => {
nextTick(() => {
if (nameRef.value && titleRef.value && generateDataRef.value) {
const lineHeight = 22.5
const padding = 8
const twoLineHeight = 55
const generateDataWidth = generateDataRef.value.offsetWidth + 20
nameRef.value.style.maxHeight = 'none'
nameRef.value.style.overflow = 'visible'
const actualHeight = nameRef.value.scrollHeight
const lineCount = Math.ceil((actualHeight - padding * 2) / lineHeight)
if (!isHovering.value) {
nameRef.value.style.maxHeight = '55px'
nameRef.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
} else {
shouldShowOnSecondLine.value = false
wrapperHeight.value = Math.max(lineHeight + padding * 2, actualHeight)
}
}
}
})
}
watch(isHovering, (newVal) => {
if (nameRef.value) {
if (newVal) {
nameRef.value.style.maxHeight = 'none'
nameRef.value.style.overflow = 'visible'
} else {
nameRef.value.style.maxHeight = '55px'
nameRef.value.style.overflow = 'hidden'
}
}
})
watch(() => props.item.generateData.prompt, () => {
checkTextOverflow()
}, { immediate: true })
onMounted(() => {
checkTextOverflow()
window.addEventListener('resize', checkTextOverflow)
})
onUnmounted(() => {
window.removeEventListener('resize', checkTextOverflow)
})
const isCollected = (url) => { const isCollected = (url) => {
return localCollectStatus.value[url] === true return localCollectStatus.value[url] === true
@ -132,12 +204,12 @@ const AIbrush = (file, index) => {
} }
const reEdit = () => { const reEdit = () => {
useDisplay.setResultData(props.item.result) useDisplay.setResultData(props.item.generateData)
useDisplay.fillParamsForEdit() useDisplay.fillParamsForEdit()
} }
const againGenerate = () => { const againGenerate = () => {
useDisplay.setResultData(props.item.result) useDisplay.setResultData(props.item.generateData)
useDisplay.triggerGenerateWithResult() useDisplay.triggerGenerateWithResult()
} }
@ -214,40 +286,62 @@ const addCollection = async (url) => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 1; flex: 1;
gap: 20px; gap: 12px;
padding-bottom: 20px; padding-bottom: 40px;
} }
.none-primary-box{ .none-primary-box{
height: 350px; height: 350px;
} }
.title{ .title{
// height: 25px;
width: 100%; width: 100%;
display: flex; position: relative;
align-items: center;
gap: 20px;
.style{ .prompt-wrapper{
width: 100%; position: relative;
display: inline; min-height: 28.5px;
overflow: visible;
} }
.name{ .prompt{
position: absolute;
left: 0;
top: 0;
color: #333; color: #333;
font-family: "Microsoft YaHei"; font-family: "Microsoft YaHei";
font-size: 15px; font-size: 14px;
font-style: normal; font-style: normal;
font-weight: 700; font-weight: 400;
line-height: 1.5; line-height: 22.5px;
word-break: break-all; word-break: break-all;
display: inline; max-height: 60px;
margin-right: 20px; 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;
}
} }
.generate-data{ .generate-data{
position: absolute;
right: 0;
top: 0;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
z-index: 2;
background-color: #fff;
padding-right: 20px;
&.second-line{
top: auto;
bottom: 0;
}
} }
.detailed-data{ .detailed-data{
@ -270,28 +364,6 @@ const addCollection = async (url) => {
border-left: none; border-left: none;
padding-left: 0; padding-left: 0;
} }
.time{
color: #333;
font-family: "Microsoft YaHei";
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: normal;
}
.dividing-line{
height: 100%;
border: 1px solid #ccc;
}
.delete-btn{
cursor: pointer;
}
.delete-btn:hover{
color: #ff4949;
}
} }
.box{ .box{
@ -399,6 +471,13 @@ const addCollection = async (url) => {
object-fit: contain; /* 确保图片完整显示,不被裁剪 */ object-fit: contain; /* 确保图片完整显示,不被裁剪 */
border-radius: 8px; /* 可选:给图片添加圆角 */ border-radius: 8px; /* 可选:给图片添加圆角 */
} }
.video{
width: 380px;
height: auto; /* 保持宽高比 */
object-fit: contain; /* 确保图片完整显示,不被裁剪 */
border-radius: 8px; /* 可选:给图片添加圆角 */
}
} }
.left-top,.bottom-brush{ .left-top,.bottom-brush{

View File

@ -50,6 +50,7 @@
:estimated-height="300" :estimated-height="300"
:buffer-size="3" :buffer-size="3"
direction="reverse" direction="reverse"
:bottom-placeholder-height="350"
class="scroller" class="scroller"
@scroll="handleScroll" @scroll="handleScroll"
@visible-change="handleVisibleChange" @visible-change="handleVisibleChange"
@ -103,7 +104,7 @@ const isInitializing = ref(true)
const { canvasVisible, canvasImage, canvasReferenceImages, canvasSource } = storeToRefs(useDisplay) const { canvasVisible, canvasImage, canvasReferenceImages, canvasSource } = storeToRefs(useDisplay)
const chargeType = computed(() => getChargeType(props.type)) const chargeType = computed(() => getChargeType(props.type))
console.log(chargeType.value) // console.log(chargeType.value)
const timeOptions = [ const timeOptions = [
{ label: '全部', value: 'all' }, { label: '全部', value: 'all' },
@ -135,42 +136,25 @@ const toggleDisplay = (newValue, oldValue) => {
} }
const conversion = (newlist) => { const conversion = (newlist) => {
const temp = newlist.list.map((item) => { const temp = newlist.map((item) => {
const files = item.fileUrl ? item.fileUrl.split(',').filter(url => url.trim()) : [] const files = item.fileUrl ? item.fileUrl.split(',').filter(url => url.trim()) : []
const generateData = JSON.parse(item.result || '{}')
return { return {
id: item.id, id: item.id,
taskId: item.taskId, taskId: item.taskId,
type: props.type,
collection: item.collection, collection: item.collection,
status: 'success', status: 'success',
prompt: item.prompt, generateData: generateData,
params: item.params,
result: item.result,
time: item.createTime, time: item.createTime,
files: files, files: files,
collectStatus: item.collectStatus || {}, collectStatus: item.collectStatus || {}
model: item.model || '',
proportion: item.proportion || '',
resolution: item.resolution || '',
quantity: item.quantity || 1
} }
}) })
console.log(temp)
return temp return temp
} }
const adaptDataList = (dataList) => {
const convertedList = conversion({ list: dataList })
return convertedList.map((item, index) => {
const originalItem = dataList[index]
return {
...item,
text: originalItem?.title || item.prompt || '生成图片',
name: originalItem?.title || item.prompt || '生成图片',
type: 'image',
title: originalItem?.title || '生成图片'
}
})
}
const fetchHistory = async (isLoadMore = false) => { const fetchHistory = async (isLoadMore = false) => {
if (isLoading.value || (!isLoadMore && !hasMoreData.value)) return if (isLoading.value || (!isLoadMore && !hasMoreData.value)) return
@ -200,7 +184,7 @@ const fetchHistory = async (isLoadMore = false) => {
return return
} }
const adaptedList = adaptDataList(dataList) const adaptedList = conversion(dataList)
if (isLoadMore) { if (isLoadMore) {
useDisplay.appendHistoryList(adaptedList) useDisplay.appendHistoryList(adaptedList)
@ -244,13 +228,13 @@ const fetchHistory = async (isLoadMore = false) => {
} }
} }
const handleScroll = (scrollTop, scrollInfo) => { const handleScroll = (scrollInfo) => {
if (isInitializing.value) return if (isInitializing.value) return
if (!scrollInfo) return if (!scrollInfo) return
const { isAtTop, isAtBottom, distanceToTop, distanceToBottom } = scrollInfo const { isAtPageTop, isAtPageBottom, distanceToPageTop, distanceToPageBottom } = scrollInfo
if (isAtTop && !isLoading.value && !isLoadingMoreLocked.value && hasMoreData.value) { if (isAtPageTop && !isLoading.value && !isLoadingMoreLocked.value && hasMoreData.value) {
isLoadingMoreLocked.value = true isLoadingMoreLocked.value = true
fetchHistory(true) fetchHistory(true)
setTimeout(() => { setTimeout(() => {
@ -258,9 +242,9 @@ const handleScroll = (scrollTop, scrollInfo) => {
}, 3000) }, 3000)
} }
if (isAtBottom) { if (isAtPageBottom) {
useDisplay.Sender_variant = 'updown' useDisplay.Sender_variant = 'updown'
} else if (distanceToTop >= 350) { } else if (distanceToPageTop >= 350) {
useDisplay.Sender_variant = 'default' useDisplay.Sender_variant = 'default'
} }
} }

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { computed, ref, onMounted } from 'vue' import { computed, ref, onMounted, watch } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import display from './display/index.vue' import display from './display/index.vue'
import { useDisplayStore } from '@/stores' import { useDisplayStore } from '@/stores'
@ -14,8 +14,16 @@ const Generate = computed(() => route.query.Generate || false)
const type = computed(() => route.query.type || 'painting') const type = computed(() => route.query.type || 'painting')
console.log(type.value) console.log(type.value)
watch(dialogBoxRef, (newRef) => {
if (newRef) {
useDisplay.setDialogBoxRef(newRef)
}
}, { immediate: true })
onMounted(() => { onMounted(() => {
useDisplay.setDialogBoxRef(dialogBoxRef.value) if (dialogBoxRef.value) {
useDisplay.setDialogBoxRef(dialogBoxRef.value)
}
}) })
</script> </script>