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:
DaydreamCodingandClaude Opus 4.8 authored and QTom committed 2026-06-05 14:00:57 +08:00
1 parent cfb195c7b2
commit cf12bc521b
3 files changed
+40 -8

No files matched your search

+28 -1
View File
@@ -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())