虚拟滚动未做完,新加收藏显示逻辑

This commit is contained in:
王佑琳 2026-03-27 15:27:46 +08:00
parent 70529ccd47
commit 57c8863113
9 changed files with 394 additions and 595 deletions

1
components.d.ts vendored
View File

@ -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']

344
out.txt

File diff suppressed because one or more lines are too long

View File

@ -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 })
} }

View File

@ -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

View File

@ -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([]) height: '100%',
const viewMap = new Map() width: '100%',
const unusedViews = new Map() position: 'relative'
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 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) {
computedMinSize = size
}
accumulator += size
sizesMap[i] = { accumulator, size }
} }
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(() => ({ 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.offset}px)`,
transform: `translateY(${renderItem.position}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()
itemRefs.clear()
$_startIndex = 0
$_endIndex = 0
nextTick(() => { if (newLength > oldLength) {
updateVisibleItems(true) 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 }) }, { 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 {

View File

@ -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)
} }

View File

@ -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);

View File

@ -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

View File

@ -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" />