From 3507eddfb379aa98b705331f5ef1cd08ae079ca1 Mon Sep 17 00:00:00 2001
From: WangLeo <690854599@qq.com>
Date: Tue, 9 Jun 2026 11:55:22 +0800
Subject: [PATCH] =?UTF-8?q?fix:=20=E7=A7=BB=E9=99=A4=20dialogBox=20?=
=?UTF-8?q?=E4=B8=AD=E6=AE=8B=E7=95=99=E7=9A=84=E5=B9=B3=E5=8F=B0=E5=88=86?=
=?UTF-8?q?=E6=94=AF=EF=BC=8C=E7=BB=9F=E4=B8=80=E4=B8=BA=20descriptor=20?=
=?UTF-8?q?=E6=8E=A5=E5=8F=A3=E6=96=B9=E6=B3=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/components/dialogBox/index.vue | 22 +-
.../virtual-scroller/VirtualScroller.vue | 196 +++++-------------
src/platforms/painting/index.js | 10 +-
src/platforms/video/index.js | 11 +
4 files changed, 80 insertions(+), 159 deletions(-)
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'
},