diff --git a/src/components/dialogBox/index.vue b/src/components/dialogBox/index.vue index ecea7b5..debcb60 100644 --- a/src/components/dialogBox/index.vue +++ b/src/components/dialogBox/index.vue @@ -104,16 +104,7 @@ const showUploader = computed(() => { return platform.value.showImageUploader() }) -const uploaderBindings = computed(() => { - const p = platform.value - if (p.id === 'painting') { - return { limit: p.imageUploadLimit() } - } - if (p.id === 'video') { - return { modelType: p.modelType.value, imagesCount: p.imageUploadLimit() } - } - return {} -}) +const uploaderBindings = computed(() => platform.value.getUploaderBindings()) const autoSizeConfig = computed(() => { if (useDisplay.Sender_variant !== 'default') { @@ -125,8 +116,9 @@ const autoSizeConfig = computed(() => { const handleStart = async () => { const p = platform.value - if (props.type === 'Video' && p.model.value === 'Seedance 2.0') { - ElMessage.primary('敬请期待 Seedance 2.0') + const validationError = p.validateBeforeSubmit() + if (validationError) { + ElMessage.primary(validationError) return } @@ -195,11 +187,7 @@ watch( [() => platform.value.model.value, () => platform.value.modelType.value], async ([newModel, newModelType]) => { if (!newModel) return - if (platform.value.id === 'video') { - await platform.value.loadConfig(newModel, newModelType) - } else { - platform.value.loadConfig(newModel) - } + await platform.value.loadConfig(newModel, newModelType) }, ) diff --git a/src/components/virtual-scroller/VirtualScroller.vue b/src/components/virtual-scroller/VirtualScroller.vue index 1a28515..89aeb07 100644 --- a/src/components/virtual-scroller/VirtualScroller.vue +++ b/src/components/virtual-scroller/VirtualScroller.vue @@ -26,7 +26,6 @@
{ return { @@ -138,6 +133,7 @@ const containerStyle = computed(() => { }) const totalHeight = computed(() => { + heightVersion.value // 依赖追踪:measureItem 直接 mutate Map 时通过此版本号触发重算 let height = 0 const len = computedData.value.length @@ -173,51 +169,50 @@ const visibleRange = computed(() => { if (!renderContainerRef.value || computedData.value.length === 0) { return { start: 0, end: 0, offset: 0 } } - + + heightVersion.value // 依赖追踪:measureItem 直接 mutate Map 时通过此版本号触发重算 const viewportHeight = containerHeight.value const currentScrollTop = scrollTop.value const bufferCount = computedBuffer.value - - let startIndex = 0 - let endIndex = computedData.value.length - 1 - let startOffset = 0 - + const len = computedData.value.length + + // 第一趟:定位首个可见项索引 let offset = 0 - for (let i = 0; i < computedData.value.length; i++) { - const cachedHeight = itemHeights.value.get(i) - const height = cachedHeight !== undefined ? cachedHeight : props.estimatedHeight - + let firstVisibleIdx = 0 + for (let i = 0; i < len; i++) { + const height = itemHeights.value.get(i) ?? props.estimatedHeight if (offset + height > currentScrollTop) { - startIndex = Math.max(0, i - bufferCount) + firstVisibleIdx = i break } - offset += height } - - startOffset = 0 - for (let i = 0; i < startIndex; i++) { - const cachedHeight = itemHeights.value.get(i) - startOffset += cachedHeight !== undefined ? cachedHeight : props.estimatedHeight - } - - offset = startOffset - endIndex = startIndex - for (let i = startIndex; i < computedData.value.length; i++) { - const cachedHeight = itemHeights.value.get(i) - const height = cachedHeight !== undefined ? cachedHeight : props.estimatedHeight - - offset += height - - if (offset > currentScrollTop + viewportHeight) { - endIndex = Math.min(computedData.value.length - 1, i + bufferCount) - break + + const startIndex = Math.max(0, firstVisibleIdx - bufferCount) + + // 第二趟:计算 startOffset 并同时定位 endIndex + let startOffset = 0 + let endIndex = len - 1 + offset = 0 + for (let i = 0; i < len; i++) { + const height = itemHeights.value.get(i) ?? props.estimatedHeight + + if (i < startIndex) { + startOffset += height } - - endIndex = i + + if (i >= startIndex) { + if (offset + height > currentScrollTop + viewportHeight) { + endIndex = Math.min(len - 1, i + bufferCount) + break + } + endIndex = i + } + + offset += height } - - return { start: Math.min(startIndex, endIndex), end: Math.max(startIndex, endIndex), offset: startOffset } + + return { start: startIndex, end: endIndex, offset: startOffset } }) const visibleItems = computed(() => { @@ -247,7 +242,6 @@ const wrapperStyle = computed(() => ({ direction: 'rtl', height: '100%', position: 'relative', - scrollbarWidth: 'auto', overflow: 'hidden', transform: 'rotate(180deg)', width: '100%' @@ -302,28 +296,19 @@ const getItemStyle = (renderItem) => { } } -const setItemRef = (el, index) => { - if (el) { - itemRefs.set(index, el) - } else { - itemRefs.delete(index) - } -} - const measureItem = (index, element) => { if (!element) return - + const firstChild = element.firstElementChild const targetElement = firstChild || element - + const height = Math.ceil(targetElement.offsetHeight) - + if (height > 0) { const cachedHeight = itemHeights.value.get(index) if (cachedHeight !== height) { - const newHeights = new Map(itemHeights.value) - newHeights.set(index, height) - itemHeights.value = newHeights + itemHeights.value.set(index, height) + heightVersion.value++ } } } @@ -357,29 +342,10 @@ const handleWheel = (event) => { event.preventDefault() } -const scrollCleanupTimeout = ref(null) - const handleScroll = (event) => { const target = event.target scrollTop.value = target.scrollTop - isScrolling.value = true - - if (scrollTimeout.value) { - clearTimeout(scrollTimeout.value) - } - - scrollTimeout.value = setTimeout(() => { - isScrolling.value = false - }, 150) - - // 滚动时添加防抖清理,每100ms最多执行一次 - if (scrollCleanupTimeout.value) { - clearTimeout(scrollCleanupTimeout.value) - } - scrollCleanupTimeout.value = setTimeout(() => { - cleanupExtraItems(visibleItems.value) - }, 300) - + const st = target.scrollTop const scrollHeight = target.scrollHeight const clientHeight = target.clientHeight @@ -462,7 +428,6 @@ const getVisibleIndices = () => { const resetMeasurements = () => { itemHeights.value = new Map() - itemRefs.clear() } const isAtPageBottom = () => { @@ -478,13 +443,15 @@ const isAtPageTop = () => { } const observeVisibleItems = () => { - if (!resizeObserver.value) return - + if (!resizeObserver.value || !renderContainerRef.value) return + resizeObserver.value.disconnect() - - for (const [index, element] of itemRefs) { - if (element) { - resizeObserver.value.observe(element) + + // 基于 visibleItems 的 index 通过 DOM 查询定位元素,避免 itemRefs 残留旧 DOM 引用 + for (const item of visibleItems.value) { + const el = renderContainerRef.value.querySelector(`[data-index="${item.index}"]`) + if (el) { + resizeObserver.value.observe(el) } } } @@ -492,31 +459,28 @@ const observeVisibleItems = () => { watch(() => computedData.value, (newData, oldData) => { const oldLength = oldData?.length || 0 const newLength = newData.length - + if (newLength !== oldLength) { const newHeights = new Map() - + const minLen = Math.min(oldLength, newLength) for (let i = 0; i < minLen; i++) { if (itemHeights.value.has(i)) { newHeights.set(i, itemHeights.value.get(i)) } } - + itemHeights.value = newHeights - + nextTick(() => { observeVisibleItems() }) } - - previousDataLength.value = newLength }, { deep: false }) watch(visibleItems, (newItems) => { nextTick(() => { observeVisibleItems() - cleanupExtraItems(newItems) }) if (newItems.length > 0) { const firstItem = newItems[0] @@ -525,56 +489,16 @@ watch(visibleItems, (newItems) => { } }, { deep: true }) -const cleanupExtraItems = (currentVisibleItems) => { - if (!renderContainerRef.value || !currentVisibleItems.length) return - - // 构建当前应该可见的索引集合 - const visibleIndices = new Set(currentVisibleItems.map(item => item.index)) - - // 直接获取 render-container 内所有实际渲染的 .virtual-scroller-item 元素 - const renderedItems = renderContainerRef.value.querySelectorAll('.virtual-scroller-item') - - const toRemove = [] - - for (const el of renderedItems) { - const dataIndex = parseInt(el.getAttribute('data-index'), 10) - - // 如果元素的 data-index 不在可见范围内,标记为删除 - if (!isNaN(dataIndex) && !visibleIndices.has(dataIndex)) { - toRemove.push(el) - } - } - - // 从 DOM 中删除多余元素 - for (const el of toRemove) { - if (el.parentNode) { - el.parentNode.removeChild(el) - } - // 同步清理 itemRefs Map - const index = parseInt(el.getAttribute('data-index'), 10) - if (!isNaN(index)) { - itemRefs.delete(index) - } - } - - if (toRemove.length > 0) { - console.log(`[VirtualScroller] 清理了 ${toRemove.length} 个多余DOM元素`) - } -} - onMounted(() => { setupResizeObserver() - isInitialized.value = true - previousDataLength.value = computedData.value.length - + nextTick(() => { if (pendingScrollToBottom.value) { pendingScrollToBottom.value = false scrollToBottom() } - + observeVisibleItems() - cleanupExtraItems(visibleItems.value) }) }) @@ -582,16 +506,6 @@ onBeforeUnmount(() => { if (resizeObserver.value) { resizeObserver.value.disconnect() } - - if (scrollTimeout.value) { - clearTimeout(scrollTimeout.value) - } - - if (scrollCleanupTimeout.value) { - clearTimeout(scrollCleanupTimeout.value) - } - - itemRefs.clear() }) defineExpose({ diff --git a/src/platforms/painting/index.js b/src/platforms/painting/index.js index e9dac04..c99e0c3 100644 --- a/src/platforms/painting/index.js +++ b/src/platforms/painting/index.js @@ -191,7 +191,7 @@ export function definePaintingPlatform() { return fetchPlatformModels(code) }, - async loadConfig(modelName) { + async loadConfig(modelName, _modelType) { const config = getModelConfig(modelName) syncDefaults(config) return config @@ -201,6 +201,14 @@ export function definePaintingPlatform() { return 'Flux 2' }, + validateBeforeSubmit() { + return null // 无阻塞,返回 null 表示通过 + }, + + getUploaderBindings() { + return { limit: imageUploadLimit() } + }, + showImageUploader() { if (modelType.value !== 'text') return true return modelConfig.value?.inputType === 'image' || modelConfig.value?.inputType === 'both' diff --git a/src/platforms/video/index.js b/src/platforms/video/index.js index b2cf613..496137e 100644 --- a/src/platforms/video/index.js +++ b/src/platforms/video/index.js @@ -127,6 +127,17 @@ export function defineVideoPlatform() { return 'LTX2.0' }, + validateBeforeSubmit() { + if (model.value === 'Seedance 2.0') { + return '敬请期待 Seedance 2.0' + } + return null // 通过 + }, + + getUploaderBindings() { + return { modelType: modelType.value, imagesCount: imageUploadLimit() } + }, + showImageUploader() { return modelType.value !== 'text' },