虚拟滚动未做完,新加收藏显示逻辑
This commit is contained in:
parent
70529ccd47
commit
57c8863113
|
|
@ -12,6 +12,7 @@ export {}
|
|||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
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']
|
||||
|
|
|
|||
|
|
@ -7,5 +7,5 @@ export function getGenerateHistoryList(query) {
|
|||
|
||||
// 取消或收藏
|
||||
export function cancelOrCollect(query) {
|
||||
return service.post('/collect/toggle', query)
|
||||
return service.post('/collect/toggle', null, { params: query })
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.1494 2.5C13.039 2.5 14.4998 3.96763 14.5 5.88477C14.5 7.06494 13.9819 8.17537 12.9678 9.4209C11.9479 10.6736 10.4752 12.006 8.64648 13.6387C8.63893 13.6454 8.63116 13.652 8.62402 13.6592L8.35547 13.9307C8.15984 14.1281 7.84016 14.1281 7.64453 13.9307L7.37598 13.6592L7.35352 13.6387L6.05078 12.4668C4.81731 11.3444 3.79719 10.3604 3.03223 9.4209C2.01813 8.17537 1.5 7.06494 1.5 5.88477C1.50024 3.9673 2.9594 2.5 4.84863 2.5C5.88613 2.5 6.93824 2.99756 7.61523 3.80469C7.71024 3.91796 7.85021 3.9834 7.99805 3.9834C8.14588 3.9834 8.28586 3.91796 8.38086 3.80469C9.05777 2.99766 10.1098 2.5 11.1494 2.5Z" fill="#FF4D4F"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 736 B |
|
|
@ -13,8 +13,10 @@
|
|||
ref="renderContainerRef"
|
||||
class="virtual-scroller-render-container"
|
||||
:style="renderContainerStyle"
|
||||
@scroll.passive="handleScroll"
|
||||
@wheel="handleWheel"
|
||||
>
|
||||
<div class="virtual-scroller-spacer" :style="{ height: `${totalSize}px` }"></div>
|
||||
<div class="virtual-scroller-spacer" :style="{ height: `${totalHeight}px` }"></div>
|
||||
<div
|
||||
class="virtual-scroller-bottom-placeholder"
|
||||
:style="bottomPlaceholderStyle"
|
||||
|
|
@ -22,18 +24,18 @@
|
|||
<slot name="bottom-placeholder" />
|
||||
</div>
|
||||
<div
|
||||
v-for="renderItem in pool"
|
||||
:key="renderItem.nr.key"
|
||||
:ref="el => setItemRef(el, renderItem.nr.key)"
|
||||
v-for="renderItem in visibleItems"
|
||||
:key="renderItem.key"
|
||||
:ref="el => setItemRef(el, renderItem.key)"
|
||||
class="virtual-scroller-item"
|
||||
:style="getItemStyle(renderItem)"
|
||||
:data-index="renderItem.nr.index"
|
||||
:data-key="renderItem.nr.key"
|
||||
:data-index="renderItem.index"
|
||||
:data-key="renderItem.key"
|
||||
>
|
||||
<slot
|
||||
name="default"
|
||||
:item="renderItem.item"
|
||||
:index="renderItem.nr.index"
|
||||
:index="renderItem.index"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -42,7 +44,7 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, shallowReactive, watch, markRaw } from 'vue'
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
|
|
@ -56,15 +58,11 @@ const props = defineProps({
|
|||
},
|
||||
estimatedHeight: {
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
minItemSize: {
|
||||
type: [Number, String],
|
||||
default: null
|
||||
default: 100
|
||||
},
|
||||
buffer: {
|
||||
type: Number,
|
||||
default: 200
|
||||
default: 3
|
||||
},
|
||||
height: {
|
||||
type: [String, Number],
|
||||
|
|
@ -85,38 +83,23 @@ const props = defineProps({
|
|||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['scroll', 'scroll-start', 'scroll-end', 'resize', 'update'])
|
||||
const emit = defineEmits(['scroll', 'scroll-start', 'scroll-end'])
|
||||
|
||||
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 isScrolling = ref(false)
|
||||
const scrollTimeout = ref(null)
|
||||
const isInitialized = ref(false)
|
||||
const pendingScrollToBottom = ref(false)
|
||||
|
||||
let uid = 0
|
||||
const pool = ref([])
|
||||
const viewMap = new Map()
|
||||
const unusedViews = new Map()
|
||||
const itemSizeMap = ref(new Map())
|
||||
const totalSize = ref(0)
|
||||
|
||||
let $_startIndex = 0
|
||||
let $_endIndex = 0
|
||||
let $_scrollDirty = false
|
||||
let $_lastUpdateScrollPosition = 0
|
||||
|
||||
const containerStyle = computed(() => {
|
||||
return {
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
position: 'relative'
|
||||
}
|
||||
})
|
||||
const containerStyle = computed(() => ({
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
position: 'relative'
|
||||
}))
|
||||
|
||||
const getItemKey = (item, index) => {
|
||||
if (typeof props.itemKey === 'function') {
|
||||
|
|
@ -128,38 +111,153 @@ const getItemKey = (item, index) => {
|
|||
return index
|
||||
}
|
||||
|
||||
const minSize = computed(() => {
|
||||
if (props.minItemSize) {
|
||||
return typeof props.minItemSize === 'string' ? parseInt(props.minItemSize, 10) : props.minItemSize
|
||||
const totalHeight = computed(() => {
|
||||
let height = props.bottomPlaceholderHeight
|
||||
const len = props.data.length
|
||||
|
||||
for (let i = 0; i < len; i++) {
|
||||
const key = getItemKey(props.data[i], i)
|
||||
const cachedHeight = itemHeights.value.get(key)
|
||||
height += cachedHeight !== undefined ? cachedHeight : props.estimatedHeight
|
||||
}
|
||||
return props.estimatedHeight || 50
|
||||
|
||||
return height
|
||||
})
|
||||
|
||||
const sizes = computed(() => {
|
||||
const sizesMap = {
|
||||
'-1': { accumulator: 0 }
|
||||
}
|
||||
const items = props.data
|
||||
let accumulator = 0
|
||||
let computedMinSize = 10000
|
||||
const getItemPosition = (index) => {
|
||||
let offset = 0
|
||||
|
||||
for (let i = 0, l = items.length; i < l; i++) {
|
||||
const key = getItemKey(items[i], i)
|
||||
let size = itemSizeMap.value.get(key)
|
||||
|
||||
if (size === undefined) {
|
||||
size = minSize.value
|
||||
}
|
||||
|
||||
if (size < computedMinSize) {
|
||||
computedMinSize = size
|
||||
}
|
||||
|
||||
accumulator += size
|
||||
sizesMap[i] = { accumulator, size }
|
||||
for (let i = 0; i < index; i++) {
|
||||
const key = getItemKey(props.data[i], i)
|
||||
const cachedHeight = itemHeights.value.get(key)
|
||||
offset += cachedHeight !== undefined ? cachedHeight : props.estimatedHeight
|
||||
}
|
||||
|
||||
return sizesMap
|
||||
const key = getItemKey(props.data[index], index)
|
||||
const height = itemHeights.value.get(key) ?? props.estimatedHeight
|
||||
|
||||
return { offset, height }
|
||||
}
|
||||
|
||||
const containerHeight = computed(() => {
|
||||
if (!renderContainerRef.value) return 0
|
||||
return renderContainerRef.value.clientHeight
|
||||
})
|
||||
|
||||
const visibleRange = computed(() => {
|
||||
if (props.data.length === 0) {
|
||||
return { start: 0, end: 0, offset: 0 }
|
||||
}
|
||||
|
||||
const el = renderContainerRef.value
|
||||
if (!el) {
|
||||
const count = props.data.length
|
||||
return {
|
||||
start: Math.max(0, count - 10),
|
||||
end: count - 1,
|
||||
offset: 0
|
||||
}
|
||||
}
|
||||
|
||||
const currentScrollTop = el.scrollTop
|
||||
const viewportHeight = containerHeight.value || el.clientHeight || 600
|
||||
const bufferCount = props.buffer
|
||||
const count = props.data.length
|
||||
|
||||
const extraBuffer = Math.ceil(viewportHeight / props.estimatedHeight)
|
||||
|
||||
const placeholderHeight = props.bottomPlaceholderHeight
|
||||
const scrollHeight = el.scrollHeight
|
||||
|
||||
const maxScrollTop = Math.max(0, scrollHeight - viewportHeight)
|
||||
const normalizedScrollTop = Math.min(Math.max(0, currentScrollTop), maxScrollTop)
|
||||
|
||||
const visibleStart = Math.max(0, normalizedScrollTop)
|
||||
const visibleEnd = visibleStart + viewportHeight
|
||||
|
||||
let startIndex = 0
|
||||
let endIndex = count - 1
|
||||
let startOffset = 0
|
||||
let currentOffset = 0
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const key = getItemKey(props.data[i], i)
|
||||
const height = itemHeights.value.get(key) ?? props.estimatedHeight
|
||||
|
||||
const itemEnd = currentOffset + height
|
||||
|
||||
if (itemEnd > visibleStart) {
|
||||
startIndex = Math.max(0, i - bufferCount - extraBuffer)
|
||||
break
|
||||
}
|
||||
|
||||
currentOffset += height
|
||||
}
|
||||
|
||||
startOffset = 0
|
||||
for (let i = 0; i < startIndex; i++) {
|
||||
const key = getItemKey(props.data[i], i)
|
||||
startOffset += itemHeights.value.get(key) ?? props.estimatedHeight
|
||||
}
|
||||
|
||||
currentOffset = startOffset
|
||||
for (let i = startIndex; i < count; i++) {
|
||||
const key = getItemKey(props.data[i], i)
|
||||
const height = itemHeights.value.get(key) ?? props.estimatedHeight
|
||||
|
||||
if (currentOffset > visibleEnd) {
|
||||
endIndex = Math.min(count - 1, i - 1 + bufferCount + extraBuffer)
|
||||
break
|
||||
}
|
||||
|
||||
currentOffset += height
|
||||
endIndex = i
|
||||
}
|
||||
|
||||
return { start: startIndex, end: endIndex, offset: startOffset }
|
||||
})
|
||||
|
||||
const visibleItems = computed(() => {
|
||||
const { start, end, offset } = visibleRange.value
|
||||
const items = []
|
||||
|
||||
if (!props.data || props.data.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
const safeStart = Math.max(0, start)
|
||||
const safeEnd = Math.min(props.data.length - 1, end)
|
||||
|
||||
if (safeStart > safeEnd) {
|
||||
return []
|
||||
}
|
||||
|
||||
let currentOffset = offset
|
||||
const seenKeys = new Set()
|
||||
|
||||
for (let i = safeStart; i <= safeEnd; i++) {
|
||||
const item = props.data[i]
|
||||
if (!item) continue
|
||||
|
||||
const key = getItemKey(item, i)
|
||||
if (seenKeys.has(key)) continue
|
||||
seenKeys.add(key)
|
||||
|
||||
const cachedHeight = itemHeights.value.get(key)
|
||||
const height = cachedHeight !== undefined ? cachedHeight : props.estimatedHeight
|
||||
|
||||
items.push({
|
||||
item,
|
||||
index: i,
|
||||
key,
|
||||
offset: currentOffset,
|
||||
height
|
||||
})
|
||||
|
||||
currentOffset += height
|
||||
}
|
||||
|
||||
return items
|
||||
})
|
||||
|
||||
const wrapperStyle = computed(() => ({
|
||||
|
|
@ -194,21 +292,18 @@ const bottomPlaceholderStyle = computed(() => ({
|
|||
top: 0,
|
||||
width: '100%',
|
||||
height: `${props.bottomPlaceholderHeight}px`,
|
||||
transform: `translateY(0px)`,
|
||||
zIndex: 1
|
||||
}))
|
||||
|
||||
const getItemStyle = (renderItem) => {
|
||||
return {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
width: '100%',
|
||||
transform: `translateY(${renderItem.position}px)`,
|
||||
willChange: 'transform'
|
||||
}
|
||||
}
|
||||
const getItemStyle = (renderItem) => ({
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
width: '100%',
|
||||
transform: `translateY(${renderItem.offset}px)`,
|
||||
willChange: 'transform'
|
||||
})
|
||||
|
||||
const setItemRef = (el, key) => {
|
||||
if (el) {
|
||||
|
|
@ -218,182 +313,6 @@ const setItemRef = (el, key) => {
|
|||
}
|
||||
}
|
||||
|
||||
const addView = (index, item, key) => {
|
||||
const nr = markRaw({
|
||||
id: uid++,
|
||||
index,
|
||||
used: true,
|
||||
key
|
||||
})
|
||||
const view = shallowReactive({
|
||||
item,
|
||||
position: 0,
|
||||
nr
|
||||
})
|
||||
pool.value.push(view)
|
||||
return view
|
||||
}
|
||||
|
||||
const unuseView = (view) => {
|
||||
view.nr.used = false
|
||||
view.position = -9999
|
||||
}
|
||||
|
||||
const getScroll = () => {
|
||||
const el = renderContainerRef.value
|
||||
if (!el) return { start: 0, end: 0 }
|
||||
|
||||
return {
|
||||
start: el.scrollTop,
|
||||
end: el.scrollTop + el.clientHeight
|
||||
}
|
||||
}
|
||||
|
||||
const updateVisibleItems = (checkItem = false, checkPositionDiff = false) => {
|
||||
const items = props.data
|
||||
const count = items.length
|
||||
const sizesMap = sizes.value
|
||||
const keyField = typeof props.itemKey === 'string' ? props.itemKey : 'id'
|
||||
|
||||
if (!count) {
|
||||
pool.value = []
|
||||
viewMap.clear()
|
||||
totalSize.value = props.bottomPlaceholderHeight
|
||||
return { continuous: true }
|
||||
}
|
||||
|
||||
const el = renderContainerRef.value
|
||||
if (!el) return { continuous: true }
|
||||
|
||||
const scrollHeight = el.scrollHeight
|
||||
const scrollTop = el.scrollTop
|
||||
const clientHeight = el.clientHeight
|
||||
|
||||
const visualScrollStart = scrollHeight - scrollTop - clientHeight
|
||||
const visualScrollEnd = scrollHeight - scrollTop
|
||||
|
||||
if (checkPositionDiff) {
|
||||
let positionDiff = visualScrollStart - $_lastUpdateScrollPosition
|
||||
if (positionDiff < 0) positionDiff = -positionDiff
|
||||
if (positionDiff < minSize.value) {
|
||||
return { continuous: true }
|
||||
}
|
||||
}
|
||||
|
||||
$_lastUpdateScrollPosition = visualScrollStart
|
||||
|
||||
const buffer = props.buffer
|
||||
let scrollStart = visualScrollStart - buffer
|
||||
let scrollEnd = visualScrollEnd + buffer
|
||||
|
||||
scrollStart -= props.bottomPlaceholderHeight
|
||||
scrollEnd += props.bottomPlaceholderHeight
|
||||
|
||||
scrollStart = Math.max(0, scrollStart)
|
||||
|
||||
let startIndex = 0
|
||||
let endIndex = count - 1
|
||||
let newTotalSize = 0
|
||||
|
||||
let a = 0
|
||||
let b = count - 1
|
||||
let i = ~~(count / 2)
|
||||
let oldI
|
||||
|
||||
do {
|
||||
oldI = i
|
||||
const h = sizesMap[i]?.accumulator || 0
|
||||
if (h < scrollStart) {
|
||||
a = i
|
||||
} else if (i < count - 1 && (sizesMap[i + 1]?.accumulator || 0) > scrollStart) {
|
||||
b = i
|
||||
}
|
||||
i = ~~((a + b) / 2)
|
||||
} while (i !== oldI)
|
||||
|
||||
i < 0 && (i = 0)
|
||||
startIndex = i
|
||||
|
||||
newTotalSize = sizesMap[count - 1]?.accumulator || count * minSize.value
|
||||
|
||||
for (endIndex = i; endIndex < count && (sizesMap[endIndex]?.accumulator || 0) < scrollEnd; endIndex++);
|
||||
|
||||
if (endIndex === -1) {
|
||||
endIndex = count - 1
|
||||
} else {
|
||||
endIndex++
|
||||
endIndex > count && (endIndex = count)
|
||||
}
|
||||
|
||||
totalSize.value = newTotalSize + props.bottomPlaceholderHeight
|
||||
|
||||
const continuous = startIndex <= $_endIndex && endIndex >= $_startIndex
|
||||
|
||||
if (continuous) {
|
||||
for (let j = 0, l = pool.value.length; j < l; j++) {
|
||||
const view = pool.value[j]
|
||||
if (view.nr.used) {
|
||||
if (checkItem) {
|
||||
const newKey = getItemKey(view.item, view.nr.index)
|
||||
let newIndex = -1
|
||||
for (let k = 0; k < count; k++) {
|
||||
if (getItemKey(items[k], k) === newKey) {
|
||||
newIndex = k
|
||||
break
|
||||
}
|
||||
}
|
||||
view.nr.index = newIndex
|
||||
}
|
||||
|
||||
if (
|
||||
view.nr.index == null ||
|
||||
view.nr.index < startIndex ||
|
||||
view.nr.index >= endIndex
|
||||
) {
|
||||
unuseView(view)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let j = startIndex; j < endIndex; j++) {
|
||||
const item = items[j]
|
||||
if (!item) continue
|
||||
|
||||
const key = getItemKey(item, j)
|
||||
let view = viewMap.get(key)
|
||||
|
||||
if (!view) {
|
||||
view = addView(j, item, key)
|
||||
viewMap.set(key, view)
|
||||
} else {
|
||||
if (!view.nr.used) {
|
||||
view.nr.used = true
|
||||
}
|
||||
}
|
||||
|
||||
view.item = item
|
||||
view.nr.index = j
|
||||
view.nr.key = key
|
||||
|
||||
const prevSize = j > 0 ? sizesMap[j - 1] : { accumulator: 0 }
|
||||
view.position = (prevSize?.accumulator || 0) + props.bottomPlaceholderHeight
|
||||
}
|
||||
|
||||
pool.value = pool.value.filter(v => v.nr.used)
|
||||
|
||||
$_startIndex = startIndex
|
||||
$_endIndex = endIndex
|
||||
|
||||
emit('update', startIndex, endIndex)
|
||||
|
||||
nextTick(() => {
|
||||
observeVisibleItems()
|
||||
})
|
||||
|
||||
return { continuous }
|
||||
}
|
||||
|
||||
const measureItem = (key, element) => {
|
||||
if (!element) return
|
||||
|
||||
|
|
@ -403,11 +322,11 @@ const measureItem = (key, element) => {
|
|||
const height = targetElement.getBoundingClientRect().height
|
||||
|
||||
if (height > 0) {
|
||||
const currentSize = itemSizeMap.value.get(key)
|
||||
if (currentSize !== height) {
|
||||
const newSizes = new Map(itemSizeMap.value)
|
||||
newSizes.set(key, height)
|
||||
itemSizeMap.value = newSizes
|
||||
const cachedHeight = itemHeights.value.get(key)
|
||||
if (cachedHeight !== height) {
|
||||
const newHeights = new Map(itemHeights.value)
|
||||
newHeights.set(key, height)
|
||||
itemHeights.value = newHeights
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -418,22 +337,12 @@ const setupResizeObserver = () => {
|
|||
}
|
||||
|
||||
resizeObserver.value = new ResizeObserver((entries) => {
|
||||
let hasChanges = false
|
||||
for (const entry of entries) {
|
||||
const key = entry.target.dataset.key
|
||||
if (key !== undefined) {
|
||||
const oldSize = itemSizeMap.value.get(key)
|
||||
measureItem(key, entry.target)
|
||||
const newSize = itemSizeMap.value.get(key)
|
||||
if (oldSize !== newSize) {
|
||||
hasChanges = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasChanges) {
|
||||
updateVisibleItems(false)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -452,17 +361,15 @@ const handleWheel = (event) => {
|
|||
}
|
||||
|
||||
const handleScroll = (event) => {
|
||||
if (!$_scrollDirty) {
|
||||
$_scrollDirty = true
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
$_scrollDirty = false
|
||||
updateVisibleItems(false, true)
|
||||
})
|
||||
}
|
||||
|
||||
const target = event.target
|
||||
scrollTop.value = target.scrollTop
|
||||
const scrollHeight = target.scrollHeight
|
||||
const currentScrollTop = target.scrollTop
|
||||
const viewportHeight = target.clientHeight
|
||||
|
||||
const visualScrollBottom = scrollHeight - currentScrollTop - viewportHeight
|
||||
|
||||
scrollTop.value = visualScrollBottom
|
||||
|
||||
isScrolling.value = true
|
||||
|
||||
if (scrollTimeout.value) {
|
||||
|
|
@ -474,22 +381,23 @@ const handleScroll = (event) => {
|
|||
}, 150)
|
||||
|
||||
const st = target.scrollTop
|
||||
const scrollHeight = target.scrollHeight
|
||||
const clientHeight = target.clientHeight
|
||||
const sh = target.scrollHeight
|
||||
const ch = target.clientHeight
|
||||
|
||||
const distanceToContainerTop = st
|
||||
const distanceToContainerBottom = scrollHeight - st - clientHeight
|
||||
const distanceToContainerBottom = sh - st - ch
|
||||
|
||||
const distanceToPageTop = distanceToContainerBottom
|
||||
const distanceToPageBottom = distanceToContainerTop
|
||||
const isAtPageTop = distanceToPageTop <= 0
|
||||
const isAtPageBottom = distanceToPageBottom <= 0
|
||||
const threshold = 5
|
||||
const isAtPageTop = distanceToPageTop <= threshold
|
||||
const isAtPageBottom = distanceToPageBottom <= threshold
|
||||
|
||||
emit('scroll', {
|
||||
target,
|
||||
scrollTop: st,
|
||||
scrollTop: visualScrollBottom,
|
||||
scrollHeight,
|
||||
clientHeight,
|
||||
clientHeight: ch,
|
||||
distanceToPageTop,
|
||||
distanceToPageBottom,
|
||||
isAtPageTop,
|
||||
|
|
@ -508,20 +416,19 @@ const handleScroll = (event) => {
|
|||
const scrollToIndex = (index, behavior = 'auto') => {
|
||||
if (!renderContainerRef.value || index < 0 || index >= props.data.length) return
|
||||
|
||||
const sizesMap = sizes.value
|
||||
const offset = index > 0 ? (sizesMap[index - 1]?.accumulator || 0) : 0
|
||||
const position = getItemPosition(index)
|
||||
const el = renderContainerRef.value
|
||||
const scrollHeight = el.scrollHeight
|
||||
const targetScrollTop = scrollHeight - position.offset - props.bottomPlaceholderHeight - el.clientHeight
|
||||
|
||||
renderContainerRef.value.scrollTo({
|
||||
top: offset,
|
||||
el.scrollTo({
|
||||
top: targetScrollTop,
|
||||
behavior
|
||||
})
|
||||
}
|
||||
|
||||
const scrollToBottom = (behavior = 'smooth') => {
|
||||
if (!renderContainerRef.value) {
|
||||
pendingScrollToBottom.value = true
|
||||
return
|
||||
}
|
||||
if (!renderContainerRef.value) return
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
if (!renderContainerRef.value) return
|
||||
|
|
@ -550,33 +457,26 @@ const scrollToTop = (behavior = 'smooth') => {
|
|||
const getScrollElement = () => renderContainerRef.value
|
||||
|
||||
const getVisibleIndices = () => {
|
||||
const indices = []
|
||||
for (let i = $_startIndex; i < $_endIndex; i++) {
|
||||
indices.push(i)
|
||||
}
|
||||
return indices
|
||||
const { start, end } = visibleRange.value
|
||||
return Array.from({ length: end - start + 1 }, (_, i) => start + i)
|
||||
}
|
||||
|
||||
const resetMeasurements = () => {
|
||||
itemSizeMap.value = new Map()
|
||||
itemHeights.value = new Map()
|
||||
itemRefs.clear()
|
||||
pool.value = []
|
||||
viewMap.clear()
|
||||
scrollTop.value = 0
|
||||
$_startIndex = 0
|
||||
$_endIndex = 0
|
||||
}
|
||||
|
||||
const isAtPageBottom = () => {
|
||||
if (!renderContainerRef.value) return false
|
||||
const { scrollTop } = renderContainerRef.value
|
||||
return scrollTop <= 0
|
||||
return scrollTop <= 5
|
||||
}
|
||||
|
||||
const isAtPageTop = () => {
|
||||
if (!renderContainerRef.value) return false
|
||||
const { scrollTop, scrollHeight, clientHeight } = renderContainerRef.value
|
||||
return scrollHeight - scrollTop - clientHeight <= 0
|
||||
return scrollHeight - scrollTop - clientHeight <= 5
|
||||
}
|
||||
|
||||
const observeVisibleItems = () => {
|
||||
|
|
@ -591,34 +491,62 @@ const observeVisibleItems = () => {
|
|||
}
|
||||
}
|
||||
|
||||
watch(() => props.data, () => {
|
||||
itemSizeMap.value = new Map()
|
||||
pool.value = []
|
||||
viewMap.clear()
|
||||
itemRefs.clear()
|
||||
$_startIndex = 0
|
||||
$_endIndex = 0
|
||||
watch(() => props.data, (newData, oldData) => {
|
||||
const newLength = newData?.length || 0
|
||||
const oldLength = oldData?.length || 0
|
||||
|
||||
nextTick(() => {
|
||||
updateVisibleItems(true)
|
||||
})
|
||||
if (newLength > oldLength) {
|
||||
nextTick(() => {
|
||||
observeVisibleItems()
|
||||
})
|
||||
} else if (newLength < oldLength) {
|
||||
itemHeights.value = new Map()
|
||||
itemRefs.clear()
|
||||
|
||||
nextTick(() => {
|
||||
observeVisibleItems()
|
||||
})
|
||||
} else {
|
||||
const oldKeys = new Set()
|
||||
const newKeys = new Set()
|
||||
|
||||
for (let i = 0; i < oldLength; i++) {
|
||||
oldKeys.add(getItemKey(oldData[i], i))
|
||||
}
|
||||
for (let i = 0; i < newLength; i++) {
|
||||
newKeys.add(getItemKey(newData[i], i))
|
||||
}
|
||||
|
||||
let hasChanges = false
|
||||
for (const key of newKeys) {
|
||||
if (!oldKeys.has(key)) {
|
||||
hasChanges = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (hasChanges) {
|
||||
itemHeights.value = new Map()
|
||||
itemRefs.clear()
|
||||
|
||||
nextTick(() => {
|
||||
observeVisibleItems()
|
||||
})
|
||||
}
|
||||
}
|
||||
}, { deep: true })
|
||||
|
||||
watch(sizes, () => {
|
||||
updateVisibleItems(false)
|
||||
watch(visibleItems, () => {
|
||||
nextTick(() => {
|
||||
observeVisibleItems()
|
||||
})
|
||||
}, { deep: true })
|
||||
|
||||
onMounted(() => {
|
||||
setupResizeObserver()
|
||||
isInitialized.value = true
|
||||
|
||||
nextTick(() => {
|
||||
if (pendingScrollToBottom.value) {
|
||||
pendingScrollToBottom.value = false
|
||||
scrollToBottom()
|
||||
}
|
||||
|
||||
updateVisibleItems(true)
|
||||
observeVisibleItems()
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -632,7 +560,6 @@ onBeforeUnmount(() => {
|
|||
}
|
||||
|
||||
itemRefs.clear()
|
||||
viewMap.clear()
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
|
|
@ -654,12 +581,6 @@ defineExpose({
|
|||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.virtual-scroller-wrapper {
|
||||
contain: content;
|
||||
}
|
||||
|
|
@ -669,12 +590,14 @@ defineExpose({
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.virtual-scroller-placeholder {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.virtual-scroller-render-container {
|
||||
contain: layout style;
|
||||
&::-webkit-scrollbar {
|
||||
// transform: rotate(180deg);
|
||||
display: none;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.virtual-scroller-item {
|
||||
|
|
|
|||
|
|
@ -136,7 +136,6 @@ export async function generate(type, data) {
|
|||
socket.onclose = async (event) => {
|
||||
console.log('WebSocket已关闭:', event)
|
||||
useDisplay.isSubGerenate = false
|
||||
// 清理心跳定时器
|
||||
if (heartbeatInterval) {
|
||||
clearInterval(heartbeatInterval)
|
||||
}
|
||||
|
|
@ -160,8 +159,6 @@ export async function generate(type, data) {
|
|||
} else {
|
||||
websocketError(event.code, event.reason)
|
||||
}
|
||||
// clearInterval(progressInterval.value)
|
||||
// 清理心跳定时器
|
||||
if (heartbeatInterval) {
|
||||
clearInterval(heartbeatInterval)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,14 +51,14 @@
|
|||
|
||||
<!-- 已完成 -->
|
||||
<div v-if="props.item.status === 'success'" class="box success-box">
|
||||
<div v-for="(file, index) in props.item.files" :key="index" class="one-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">
|
||||
<!-- <img :src="file" alt="index" class="img" /> -->
|
||||
<Img :src="file" alt="index" class="img" />
|
||||
|
||||
<div class="left-top">
|
||||
<div class="left-top-btn" @click="downloadImage(file, 'image')"><img src="@/assets/display/download.svg" /></div>
|
||||
<span class="line" />
|
||||
<div class="left-top-btn" @click="addCollection(file)"><img src="@/assets/display/collection.svg" /></div>
|
||||
<div v-show="hoverIndex === index" class="left-top-btn download-btn" @click="downloadImage(file, 'image')"><img src="@/assets/display/download.svg" /></div>
|
||||
<span v-if="hoverIndex === index" class="line" />
|
||||
<div class="left-top-btn collect-btn" @click="addCollection(file)"><img :src="isCollected(file) ? collectionActiveIcon : collectionIcon" /></div>
|
||||
</div>
|
||||
|
||||
<el-tooltip
|
||||
|
|
@ -86,6 +86,8 @@
|
|||
|
||||
<script setup>
|
||||
import brush from '@/assets/display/brush.svg'
|
||||
import collectionIcon from '@/assets/display/collection.svg'
|
||||
import collectionActiveIcon from '@/assets/display/collection-active.svg'
|
||||
import { useDisplayStore, useParamStore, useUserStore } from '@/stores'
|
||||
import { downloadImage } from '@/utils/downloadImage.js'
|
||||
import reEditIcon from '@/assets/display/reEdit.svg'
|
||||
|
|
@ -107,6 +109,13 @@ const useDisplay = useDisplayStore()
|
|||
const useParams = useParamStore()
|
||||
const useUser = useUserStore()
|
||||
|
||||
const localCollectStatus = ref({ ...props.item.collectStatus })
|
||||
const hoverIndex = ref(-1)
|
||||
|
||||
const isCollected = (url) => {
|
||||
return localCollectStatus.value[url] === true
|
||||
}
|
||||
|
||||
const generateStatusText = computed(() => {
|
||||
if (props.item.status === 'generate') {
|
||||
return '正在生成中...'
|
||||
|
|
@ -161,6 +170,7 @@ const addCollection = async (url) => {
|
|||
})
|
||||
if (res.success) {
|
||||
ElMessage.success(res.message || '操作成功')
|
||||
localCollectStatus.value[url] = !localCollectStatus.value[url]
|
||||
} else {
|
||||
ElMessage.error(res.message || '操作失败')
|
||||
}
|
||||
|
|
@ -346,6 +356,11 @@ const addCollection = async (url) => {
|
|||
display:flex
|
||||
}
|
||||
}
|
||||
.one-box.collected{
|
||||
.left-top{
|
||||
display:flex
|
||||
}
|
||||
}
|
||||
.success-box{
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
|
|
|
|||
|
|
@ -143,7 +143,8 @@ const conversion = (newlist) => {
|
|||
prompt: item.prompt,
|
||||
params: item.params,
|
||||
time: item.createTime,
|
||||
files: files
|
||||
files: files,
|
||||
collectStatus: item.collectStatus || {}
|
||||
}
|
||||
})
|
||||
return temp
|
||||
|
|
|
|||
|
|
@ -10,33 +10,10 @@ const loading = computed(() => route.query.loading ? false : (route.path === '/h
|
|||
const Generate = computed(() => route.query.Generate || false)
|
||||
const type = computed(() => route.query.type || 'painting')
|
||||
console.log(type.value)
|
||||
|
||||
const canvasVisible = ref(false)
|
||||
const canvasImage = ref('')
|
||||
const canvasReferenceImages = ref([])
|
||||
const canvasSource = ref('')
|
||||
|
||||
const handleOpenCanvas = (data) => {
|
||||
canvasImage.value = data.mainImage?.url || data.mainImage || ''
|
||||
canvasReferenceImages.value = data.referenceImages || []
|
||||
canvasSource.value = data.source || 'uploader'
|
||||
canvasVisible.value = true
|
||||
}
|
||||
|
||||
const handleCanvasSend = (data) => {
|
||||
console.log('Canvas send:', data)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<Canvas
|
||||
v-model:visible="canvasVisible"
|
||||
:image="canvasImage"
|
||||
:reference-images="canvasReferenceImages"
|
||||
:source="canvasSource"
|
||||
@send="handleCanvasSend"
|
||||
/>
|
||||
<dialogBox :is-generate="shouldShowDisplay" :type="type" :generate="Generate" :loading="loading" @open-canvas="handleOpenCanvas" />
|
||||
|
||||
<display :if="shouldShowDisplay" :type="type" :loading="loading" />
|
||||
|
|
|
|||
Loading…
Reference in New Issue