mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-11-06 16:50:28 +00:00
fix(usage): 用量明细虚拟表对可空字段渲染崩溃致整表空白
- formatDuration 兜底 null(duration_ms 后端为 *int,count_tokens/失败等请求无耗时) - tokens/cost 单元格与 CSV 导出加 (x ?? 0) 防御 - duration_ms 前端类型改 number|null,对齐后端指针,编译器从此拦未保护用法 - DataTable 虚拟化加固:initialRect 一屏兜底 + 过滤 0 高度读数 根因:渲染对 null 调 .toFixed() 抛错→Vue 弃整个 tbody→空白;虚拟化只渲可视行故呈概率性。 非 b3847fa 引入(formatDuration 自首个 commit 即存在)。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
cfb195c7b2
commit
cf12bc521b
3 files changed
+40
-8
No files matched your search
@@ -197,7 +197,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
||||
import { useVirtualizer } from '@tanstack/vue-virtual'
|
||||
import { useVirtualizer, observeElementRect as observeElementRectDefault } from '@tanstack/vue-virtual'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { Column } from './types'
|
||||
import Icon from '@/components/icons/Icon.vue'
|
||||
@@ -218,6 +218,27 @@ const tableWrapperRef = ref<HTMLElement | null>(null)
|
||||
const isScrollable = ref(false)
|
||||
const actionsColumnNeedsExpanding = ref(false)
|
||||
|
||||
// --- 虚拟滚动「整表空白」根治 ---
|
||||
// 根因:本组件根 .table-wrapper 为 flex:1 / min-h-0,高度由父级 flex 链决定。@tanstack 虚拟化器
|
||||
// 仅在 observeElementRect 回调里写 scrollRect;一旦该回调读到 0 高度(加载瞬间 flex 未结算,或
|
||||
// 滚动中动态行高校正触发的 reflow),scrollRect 被钉死为 0 → calculateRange 返回 null → 整表空白。
|
||||
// 对策(见下方 virtualizer 选项):
|
||||
// 1) 覆写 observeElementRect,直接丢弃 height<=0 的读数,scrollRect 永不被钉成 0;
|
||||
// 2) initialRect 给一屏兜底高度,首个有效读数到来前也有行可渲染,绝不空白。
|
||||
// 兜底高度:表格区域大致 = 视口高度 - 顶栏/外边距/筛选/分页 ≈ 320px
|
||||
const estimatedViewportHeight = () => {
|
||||
if (typeof window === 'undefined') return 600
|
||||
return Math.max(window.innerHeight - 320, 400)
|
||||
}
|
||||
|
||||
// 覆写默认 observeElementRect:过滤掉 0 高度读数(根治整表空白的关键)
|
||||
const observeElementRectNonZero = (
|
||||
instance: any,
|
||||
cb: (rect: { width: number; height: number }) => void
|
||||
) => observeElementRectDefault(instance, (rect) => {
|
||||
if (rect.height > 0) cb(rect)
|
||||
})
|
||||
|
||||
// 检查是否可滚动
|
||||
const checkScrollable = () => {
|
||||
if (tableWrapperRef.value) {
|
||||
@@ -578,6 +599,12 @@ const rowVirtualizer = useVirtualizer(computed(() => ({
|
||||
getScrollElement: () => tableWrapperRef.value,
|
||||
estimateSize: () => props.estimateRowHeight ?? 56,
|
||||
overscan: props.overscan ?? 5,
|
||||
// 兜底高度:首个有效高度读数到来前,先按一屏渲染,避免空白帧
|
||||
initialRect: { width: 0, height: estimatedViewportHeight() },
|
||||
// 关键:过滤 0 高度读数,杜绝 scrollRect 被钉成 0 → calculateRange 返回 null → 整表空白
|
||||
observeElementRect: observeElementRectNonZero,
|
||||
// 把测量类 ResizeObserver 回调批到 rAF,避免滚动中同步 reflow 风暴导致的校正抖动/空白
|
||||
useAnimationFrameWithResizeObserver: true,
|
||||
})))
|
||||
|
||||
const virtualItems = computed(() => rowVirtualizer.value.getVirtualItems())
|
||||
|
||||
Reference in new issue
Block a user