虚拟滚动未做完,新加收藏显示逻辑
This commit is contained in:
parent
70529ccd47
commit
57c8863113
|
|
@ -12,6 +12,7 @@ export {}
|
||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
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']
|
||||||
|
|
|
||||||
|
|
@ -7,5 +7,5 @@ export function getGenerateHistoryList(query) {
|
||||||
|
|
||||||
// 取消或收藏
|
// 取消或收藏
|
||||||
export function cancelOrCollect(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"
|
ref="renderContainerRef"
|
||||||
class="virtual-scroller-render-container"
|
class="virtual-scroller-render-container"
|
||||||
:style="renderContainerStyle"
|
: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
|
<div
|
||||||
class="virtual-scroller-bottom-placeholder"
|
class="virtual-scroller-bottom-placeholder"
|
||||||
:style="bottomPlaceholderStyle"
|
:style="bottomPlaceholderStyle"
|
||||||
|
|
@ -22,18 +24,18 @@
|
||||||
<slot name="bottom-placeholder" />
|
<slot name="bottom-placeholder" />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-for="renderItem in pool"
|
v-for="renderItem in visibleItems"
|
||||||
:key="renderItem.nr.key"
|
:key="renderItem.key"
|
||||||
:ref="el => setItemRef(el, renderItem.nr.key)"
|
:ref="el => setItemRef(el, renderItem.key)"
|
||||||
class="virtual-scroller-item"
|
class="virtual-scroller-item"
|
||||||
:style="getItemStyle(renderItem)"
|
:style="getItemStyle(renderItem)"
|
||||||
:data-index="renderItem.nr.index"
|
:data-index="renderItem.index"
|
||||||
:data-key="renderItem.nr.key"
|
:data-key="renderItem.key"
|
||||||
>
|
>
|
||||||
<slot
|
<slot
|
||||||
name="default"
|
name="default"
|
||||||
:item="renderItem.item"
|
:item="renderItem.item"
|
||||||
:index="renderItem.nr.index"
|
:index="renderItem.index"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -42,7 +44,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<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({
|
const props = defineProps({
|
||||||
data: {
|
data: {
|
||||||
|
|
@ -56,15 +58,11 @@ const props = defineProps({
|
||||||
},
|
},
|
||||||
estimatedHeight: {
|
estimatedHeight: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: null
|
default: 100
|
||||||
},
|
|
||||||
minItemSize: {
|
|
||||||
type: [Number, String],
|
|
||||||
default: null
|
|
||||||
},
|
},
|
||||||
buffer: {
|
buffer: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 200
|
default: 3
|
||||||
},
|
},
|
||||||
height: {
|
height: {
|
||||||
type: [String, Number],
|
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 containerRef = ref(null)
|
||||||
const wrapperRef = ref(null)
|
const wrapperRef = ref(null)
|
||||||
const renderContainerRef = ref(null)
|
const renderContainerRef = ref(null)
|
||||||
const itemRefs = new Map()
|
const itemRefs = new Map()
|
||||||
|
const itemHeights = ref(new Map())
|
||||||
const resizeObserver = ref(null)
|
const resizeObserver = ref(null)
|
||||||
const scrollTop = ref(0)
|
const scrollTop = ref(0)
|
||||||
const isScrolling = ref(false)
|
const isScrolling = ref(false)
|
||||||
const scrollTimeout = ref(null)
|
const scrollTimeout = ref(null)
|
||||||
const isInitialized = ref(false)
|
|
||||||
const pendingScrollToBottom = ref(false)
|
|
||||||
|
|
||||||
let uid = 0
|
const containerStyle = computed(() => ({
|
||||||
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%',
|
height: '100%',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
position: 'relative'
|
position: 'relative'
|
||||||
}
|
}))
|
||||||
})
|
|
||||||
|
|
||||||
const getItemKey = (item, index) => {
|
const getItemKey = (item, index) => {
|
||||||
if (typeof props.itemKey === 'function') {
|
if (typeof props.itemKey === 'function') {
|
||||||
|
|
@ -128,38 +111,153 @@ const getItemKey = (item, index) => {
|
||||||
return index
|
return index
|
||||||
}
|
}
|
||||||
|
|
||||||
const minSize = computed(() => {
|
const totalHeight = computed(() => {
|
||||||
if (props.minItemSize) {
|
let height = props.bottomPlaceholderHeight
|
||||||
return typeof props.minItemSize === 'string' ? parseInt(props.minItemSize, 10) : props.minItemSize
|
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 getItemPosition = (index) => {
|
||||||
const sizesMap = {
|
let offset = 0
|
||||||
'-1': { accumulator: 0 }
|
|
||||||
}
|
|
||||||
const items = props.data
|
|
||||||
let accumulator = 0
|
|
||||||
let computedMinSize = 10000
|
|
||||||
|
|
||||||
for (let i = 0, l = items.length; i < l; i++) {
|
for (let i = 0; i < index; i++) {
|
||||||
const key = getItemKey(items[i], i)
|
const key = getItemKey(props.data[i], i)
|
||||||
let size = itemSizeMap.value.get(key)
|
const cachedHeight = itemHeights.value.get(key)
|
||||||
|
offset += cachedHeight !== undefined ? cachedHeight : props.estimatedHeight
|
||||||
if (size === undefined) {
|
|
||||||
size = minSize.value
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (size < computedMinSize) {
|
const key = getItemKey(props.data[index], index)
|
||||||
computedMinSize = size
|
const height = itemHeights.value.get(key) ?? props.estimatedHeight
|
||||||
|
|
||||||
|
return { offset, height }
|
||||||
}
|
}
|
||||||
|
|
||||||
accumulator += size
|
const containerHeight = computed(() => {
|
||||||
sizesMap[i] = { accumulator, size }
|
if (!renderContainerRef.value) return 0
|
||||||
|
return renderContainerRef.value.clientHeight
|
||||||
|
})
|
||||||
|
|
||||||
|
const visibleRange = computed(() => {
|
||||||
|
if (props.data.length === 0) {
|
||||||
|
return { start: 0, end: 0, offset: 0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
return sizesMap
|
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(() => ({
|
const wrapperStyle = computed(() => ({
|
||||||
|
|
@ -194,21 +292,18 @@ const bottomPlaceholderStyle = computed(() => ({
|
||||||
top: 0,
|
top: 0,
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: `${props.bottomPlaceholderHeight}px`,
|
height: `${props.bottomPlaceholderHeight}px`,
|
||||||
transform: `translateY(0px)`,
|
|
||||||
zIndex: 1
|
zIndex: 1
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const getItemStyle = (renderItem) => {
|
const getItemStyle = (renderItem) => ({
|
||||||
return {
|
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
top: 0,
|
top: 0,
|
||||||
width: '100%',
|
width: '100%',
|
||||||
transform: `translateY(${renderItem.position}px)`,
|
transform: `translateY(${renderItem.offset}px)`,
|
||||||
willChange: 'transform'
|
willChange: 'transform'
|
||||||
}
|
})
|
||||||
}
|
|
||||||
|
|
||||||
const setItemRef = (el, key) => {
|
const setItemRef = (el, key) => {
|
||||||
if (el) {
|
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) => {
|
const measureItem = (key, element) => {
|
||||||
if (!element) return
|
if (!element) return
|
||||||
|
|
||||||
|
|
@ -403,11 +322,11 @@ const measureItem = (key, element) => {
|
||||||
const height = targetElement.getBoundingClientRect().height
|
const height = targetElement.getBoundingClientRect().height
|
||||||
|
|
||||||
if (height > 0) {
|
if (height > 0) {
|
||||||
const currentSize = itemSizeMap.value.get(key)
|
const cachedHeight = itemHeights.value.get(key)
|
||||||
if (currentSize !== height) {
|
if (cachedHeight !== height) {
|
||||||
const newSizes = new Map(itemSizeMap.value)
|
const newHeights = new Map(itemHeights.value)
|
||||||
newSizes.set(key, height)
|
newHeights.set(key, height)
|
||||||
itemSizeMap.value = newSizes
|
itemHeights.value = newHeights
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -418,22 +337,12 @@ const setupResizeObserver = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
resizeObserver.value = new ResizeObserver((entries) => {
|
resizeObserver.value = new ResizeObserver((entries) => {
|
||||||
let hasChanges = false
|
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
const key = entry.target.dataset.key
|
const key = entry.target.dataset.key
|
||||||
if (key !== undefined) {
|
if (key !== undefined) {
|
||||||
const oldSize = itemSizeMap.value.get(key)
|
|
||||||
measureItem(key, entry.target)
|
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) => {
|
const handleScroll = (event) => {
|
||||||
if (!$_scrollDirty) {
|
|
||||||
$_scrollDirty = true
|
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
$_scrollDirty = false
|
|
||||||
updateVisibleItems(false, true)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const target = event.target
|
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
|
isScrolling.value = true
|
||||||
|
|
||||||
if (scrollTimeout.value) {
|
if (scrollTimeout.value) {
|
||||||
|
|
@ -474,22 +381,23 @@ const handleScroll = (event) => {
|
||||||
}, 150)
|
}, 150)
|
||||||
|
|
||||||
const st = target.scrollTop
|
const st = target.scrollTop
|
||||||
const scrollHeight = target.scrollHeight
|
const sh = target.scrollHeight
|
||||||
const clientHeight = target.clientHeight
|
const ch = target.clientHeight
|
||||||
|
|
||||||
const distanceToContainerTop = st
|
const distanceToContainerTop = st
|
||||||
const distanceToContainerBottom = scrollHeight - st - clientHeight
|
const distanceToContainerBottom = sh - st - ch
|
||||||
|
|
||||||
const distanceToPageTop = distanceToContainerBottom
|
const distanceToPageTop = distanceToContainerBottom
|
||||||
const distanceToPageBottom = distanceToContainerTop
|
const distanceToPageBottom = distanceToContainerTop
|
||||||
const isAtPageTop = distanceToPageTop <= 0
|
const threshold = 5
|
||||||
const isAtPageBottom = distanceToPageBottom <= 0
|
const isAtPageTop = distanceToPageTop <= threshold
|
||||||
|
const isAtPageBottom = distanceToPageBottom <= threshold
|
||||||
|
|
||||||
emit('scroll', {
|
emit('scroll', {
|
||||||
target,
|
target,
|
||||||
scrollTop: st,
|
scrollTop: visualScrollBottom,
|
||||||
scrollHeight,
|
scrollHeight,
|
||||||
clientHeight,
|
clientHeight: ch,
|
||||||
distanceToPageTop,
|
distanceToPageTop,
|
||||||
distanceToPageBottom,
|
distanceToPageBottom,
|
||||||
isAtPageTop,
|
isAtPageTop,
|
||||||
|
|
@ -508,20 +416,19 @@ const handleScroll = (event) => {
|
||||||
const scrollToIndex = (index, behavior = 'auto') => {
|
const scrollToIndex = (index, behavior = 'auto') => {
|
||||||
if (!renderContainerRef.value || index < 0 || index >= props.data.length) return
|
if (!renderContainerRef.value || index < 0 || index >= props.data.length) return
|
||||||
|
|
||||||
const sizesMap = sizes.value
|
const position = getItemPosition(index)
|
||||||
const offset = index > 0 ? (sizesMap[index - 1]?.accumulator || 0) : 0
|
const el = renderContainerRef.value
|
||||||
|
const scrollHeight = el.scrollHeight
|
||||||
|
const targetScrollTop = scrollHeight - position.offset - props.bottomPlaceholderHeight - el.clientHeight
|
||||||
|
|
||||||
renderContainerRef.value.scrollTo({
|
el.scrollTo({
|
||||||
top: offset,
|
top: targetScrollTop,
|
||||||
behavior
|
behavior
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const scrollToBottom = (behavior = 'smooth') => {
|
const scrollToBottom = (behavior = 'smooth') => {
|
||||||
if (!renderContainerRef.value) {
|
if (!renderContainerRef.value) return
|
||||||
pendingScrollToBottom.value = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
if (!renderContainerRef.value) return
|
if (!renderContainerRef.value) return
|
||||||
|
|
@ -550,33 +457,26 @@ const scrollToTop = (behavior = 'smooth') => {
|
||||||
const getScrollElement = () => renderContainerRef.value
|
const getScrollElement = () => renderContainerRef.value
|
||||||
|
|
||||||
const getVisibleIndices = () => {
|
const getVisibleIndices = () => {
|
||||||
const indices = []
|
const { start, end } = visibleRange.value
|
||||||
for (let i = $_startIndex; i < $_endIndex; i++) {
|
return Array.from({ length: end - start + 1 }, (_, i) => start + i)
|
||||||
indices.push(i)
|
|
||||||
}
|
|
||||||
return indices
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const resetMeasurements = () => {
|
const resetMeasurements = () => {
|
||||||
itemSizeMap.value = new Map()
|
itemHeights.value = new Map()
|
||||||
itemRefs.clear()
|
itemRefs.clear()
|
||||||
pool.value = []
|
|
||||||
viewMap.clear()
|
|
||||||
scrollTop.value = 0
|
scrollTop.value = 0
|
||||||
$_startIndex = 0
|
|
||||||
$_endIndex = 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const isAtPageBottom = () => {
|
const isAtPageBottom = () => {
|
||||||
if (!renderContainerRef.value) return false
|
if (!renderContainerRef.value) return false
|
||||||
const { scrollTop } = renderContainerRef.value
|
const { scrollTop } = renderContainerRef.value
|
||||||
return scrollTop <= 0
|
return scrollTop <= 5
|
||||||
}
|
}
|
||||||
|
|
||||||
const isAtPageTop = () => {
|
const isAtPageTop = () => {
|
||||||
if (!renderContainerRef.value) return false
|
if (!renderContainerRef.value) return false
|
||||||
const { scrollTop, scrollHeight, clientHeight } = renderContainerRef.value
|
const { scrollTop, scrollHeight, clientHeight } = renderContainerRef.value
|
||||||
return scrollHeight - scrollTop - clientHeight <= 0
|
return scrollHeight - scrollTop - clientHeight <= 5
|
||||||
}
|
}
|
||||||
|
|
||||||
const observeVisibleItems = () => {
|
const observeVisibleItems = () => {
|
||||||
|
|
@ -591,34 +491,62 @@ const observeVisibleItems = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(() => props.data, () => {
|
watch(() => props.data, (newData, oldData) => {
|
||||||
itemSizeMap.value = new Map()
|
const newLength = newData?.length || 0
|
||||||
pool.value = []
|
const oldLength = oldData?.length || 0
|
||||||
viewMap.clear()
|
|
||||||
|
if (newLength > oldLength) {
|
||||||
|
nextTick(() => {
|
||||||
|
observeVisibleItems()
|
||||||
|
})
|
||||||
|
} else if (newLength < oldLength) {
|
||||||
|
itemHeights.value = new Map()
|
||||||
itemRefs.clear()
|
itemRefs.clear()
|
||||||
$_startIndex = 0
|
|
||||||
$_endIndex = 0
|
|
||||||
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
updateVisibleItems(true)
|
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 })
|
}, { deep: true })
|
||||||
|
|
||||||
watch(sizes, () => {
|
watch(visibleItems, () => {
|
||||||
updateVisibleItems(false)
|
nextTick(() => {
|
||||||
|
observeVisibleItems()
|
||||||
|
})
|
||||||
}, { deep: true })
|
}, { deep: true })
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
setupResizeObserver()
|
setupResizeObserver()
|
||||||
isInitialized.value = true
|
|
||||||
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
if (pendingScrollToBottom.value) {
|
observeVisibleItems()
|
||||||
pendingScrollToBottom.value = false
|
|
||||||
scrollToBottom()
|
|
||||||
}
|
|
||||||
|
|
||||||
updateVisibleItems(true)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -632,7 +560,6 @@ onBeforeUnmount(() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
itemRefs.clear()
|
itemRefs.clear()
|
||||||
viewMap.clear()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
|
|
@ -654,12 +581,6 @@ defineExpose({
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
-ms-overflow-style: none;
|
-ms-overflow-style: none;
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.virtual-scroller-wrapper {
|
.virtual-scroller-wrapper {
|
||||||
contain: content;
|
contain: content;
|
||||||
}
|
}
|
||||||
|
|
@ -669,12 +590,14 @@ defineExpose({
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.virtual-scroller-placeholder {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.virtual-scroller-render-container {
|
.virtual-scroller-render-container {
|
||||||
contain: layout style;
|
contain: layout style;
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
// transform: rotate(180deg);
|
||||||
|
display: none;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.virtual-scroller-item {
|
.virtual-scroller-item {
|
||||||
|
|
|
||||||
|
|
@ -136,7 +136,6 @@ export async function generate(type, data) {
|
||||||
socket.onclose = async (event) => {
|
socket.onclose = async (event) => {
|
||||||
console.log('WebSocket已关闭:', event)
|
console.log('WebSocket已关闭:', event)
|
||||||
useDisplay.isSubGerenate = false
|
useDisplay.isSubGerenate = false
|
||||||
// 清理心跳定时器
|
|
||||||
if (heartbeatInterval) {
|
if (heartbeatInterval) {
|
||||||
clearInterval(heartbeatInterval)
|
clearInterval(heartbeatInterval)
|
||||||
}
|
}
|
||||||
|
|
@ -160,8 +159,6 @@ export async function generate(type, data) {
|
||||||
} else {
|
} else {
|
||||||
websocketError(event.code, event.reason)
|
websocketError(event.code, event.reason)
|
||||||
}
|
}
|
||||||
// clearInterval(progressInterval.value)
|
|
||||||
// 清理心跳定时器
|
|
||||||
if (heartbeatInterval) {
|
if (heartbeatInterval) {
|
||||||
clearInterval(heartbeatInterval)
|
clearInterval(heartbeatInterval)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,14 +51,14 @@
|
||||||
|
|
||||||
<!-- 已完成 -->
|
<!-- 已完成 -->
|
||||||
<div v-if="props.item.status === 'success'" class="box success-box">
|
<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" /> -->
|
||||||
<Img :src="file" alt="index" class="img" />
|
<Img :src="file" alt="index" class="img" />
|
||||||
|
|
||||||
<div class="left-top">
|
<div class="left-top">
|
||||||
<div class="left-top-btn" @click="downloadImage(file, 'image')"><img src="@/assets/display/download.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 class="line" />
|
<span v-if="hoverIndex === index" class="line" />
|
||||||
<div class="left-top-btn" @click="addCollection(file)"><img src="@/assets/display/collection.svg" /></div>
|
<div class="left-top-btn collect-btn" @click="addCollection(file)"><img :src="isCollected(file) ? collectionActiveIcon : collectionIcon" /></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-tooltip
|
<el-tooltip
|
||||||
|
|
@ -86,6 +86,8 @@
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import brush from '@/assets/display/brush.svg'
|
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 { useDisplayStore, useParamStore, useUserStore } from '@/stores'
|
||||||
import { downloadImage } from '@/utils/downloadImage.js'
|
import { downloadImage } from '@/utils/downloadImage.js'
|
||||||
import reEditIcon from '@/assets/display/reEdit.svg'
|
import reEditIcon from '@/assets/display/reEdit.svg'
|
||||||
|
|
@ -107,6 +109,13 @@ const useDisplay = useDisplayStore()
|
||||||
const useParams = useParamStore()
|
const useParams = useParamStore()
|
||||||
const useUser = useUserStore()
|
const useUser = useUserStore()
|
||||||
|
|
||||||
|
const localCollectStatus = ref({ ...props.item.collectStatus })
|
||||||
|
const hoverIndex = ref(-1)
|
||||||
|
|
||||||
|
const isCollected = (url) => {
|
||||||
|
return localCollectStatus.value[url] === true
|
||||||
|
}
|
||||||
|
|
||||||
const generateStatusText = computed(() => {
|
const generateStatusText = computed(() => {
|
||||||
if (props.item.status === 'generate') {
|
if (props.item.status === 'generate') {
|
||||||
return '正在生成中...'
|
return '正在生成中...'
|
||||||
|
|
@ -161,6 +170,7 @@ const addCollection = async (url) => {
|
||||||
})
|
})
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
ElMessage.success(res.message || '操作成功')
|
ElMessage.success(res.message || '操作成功')
|
||||||
|
localCollectStatus.value[url] = !localCollectStatus.value[url]
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error(res.message || '操作失败')
|
ElMessage.error(res.message || '操作失败')
|
||||||
}
|
}
|
||||||
|
|
@ -346,6 +356,11 @@ const addCollection = async (url) => {
|
||||||
display:flex
|
display:flex
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.one-box.collected{
|
||||||
|
.left-top{
|
||||||
|
display:flex
|
||||||
|
}
|
||||||
|
}
|
||||||
.success-box{
|
.success-box{
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, 1fr);
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
|
|
||||||
|
|
@ -143,7 +143,8 @@ const conversion = (newlist) => {
|
||||||
prompt: item.prompt,
|
prompt: item.prompt,
|
||||||
params: item.params,
|
params: item.params,
|
||||||
time: item.createTime,
|
time: item.createTime,
|
||||||
files: files
|
files: files,
|
||||||
|
collectStatus: item.collectStatus || {}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return temp
|
return temp
|
||||||
|
|
|
||||||
|
|
@ -10,33 +10,10 @@ const loading = computed(() => route.query.loading ? false : (route.path === '/h
|
||||||
const Generate = computed(() => route.query.Generate || false)
|
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)
|
||||||
|
|
||||||
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="app-container">
|
<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" />
|
<dialogBox :is-generate="shouldShowDisplay" :type="type" :generate="Generate" :loading="loading" @open-canvas="handleOpenCanvas" />
|
||||||
|
|
||||||
<display :if="shouldShowDisplay" :type="type" :loading="loading" />
|
<display :if="shouldShowDisplay" :type="type" :loading="loading" />
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue