xiaoyi
9 天以前 8176bc317c5d305209f1cc5c6b975dc2ded4a540
fix(bi): 全屏留白改为 JS 量测精确填充

## 问题
上一版用 flex + grid-auto-rows: 1fr 理论上能撑满,但实际全屏后底部仍留大片空白。

## 改为确定性方案
不再依赖 CSS 弹性推断,改为运行时量测:

1. 新增 layoutFill()
- 读取 .dash-header / .kpi-row / .table-card 实际高度
- 用 window.innerHeight 减去这些占用与内边距
- 把剩余高度写入容器 CSS 变量 --fill-h
- 非全屏时移除该变量,交回普通文档流

2. .is-fullscreen > .chart-row
- 由 flex: 1 0 auto 改为 flex: 0 0 auto
- 高度改为 height: var(--fill-h, auto)
- 内层 grid-auto-rows: minmax(200px, 1fr) 均分

3. 触发时机
- onMounted 注册 window resize 监听
- toggleFullscreen 后 400ms
- fullscreenchange 后 300ms
- 数据加载完成 nextTick 后
- onBeforeUnmount 移除监听

## 验证
- vue-tsc:无新增错误(基线 318 不变,index.vue 零错误)
- 构建产物 dashboard-dCXR8cH5.css 含 fill-h,dashboard-Bd22CarL.js 含 layoutFill
- 已部署生产,前端 200
已修改1个文件
38 ■■■■■ 文件已修改
src/views/bi/dashboard/index.vue 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bi/dashboard/index.vue
@@ -49,6 +49,7 @@
      loading.value = false;
      await nextTick();
      if (!disposed && sequence === requestSequence) {
        layoutFill();
        resizeAllCharts();
        if (!background) {
          const intervals = charts.value.map((chart) => chart.refreshInterval).filter((interval) => interval > 0);
@@ -88,6 +89,31 @@
const dashboardRef = ref<HTMLElement>();
let resizeObserver: ResizeObserver | null = null;
/**
 * 精确计算图表区可用高度,写入 CSS 变量 --fill-h
 *
 * 仅靠 flex + 1fr 在部分场景不生效,这里直接量测头部、KPI 行、
 * 表格卡片占用高度,把剩余高度算出来给图表区。
 */
function layoutFill() {
  const el = dashboardRef.value;
  if (!el) return;
  if (!isFullscreen.value) {
    el.style.removeProperty('--fill-h');
    return;
  }
  const header = el.querySelector('.dash-header') as HTMLElement | null;
  const kpi = el.querySelector('.kpi-row') as HTMLElement | null;
  const tables = el.querySelectorAll<HTMLElement>('.table-card');
  const cs = getComputedStyle(el);
  let used = (parseFloat(cs.paddingTop) || 0) + (parseFloat(cs.paddingBottom) || 0);
  if (header) used += header.offsetHeight + 24;
  if (kpi) used += kpi.offsetHeight + 16;
  tables.forEach((t) => { used += t.offsetHeight + 16; });
  const avail = Math.max(240, window.innerHeight - used);
  el.style.setProperty('--fill-h', `${avail}px`);
}
function resizeAllCharts() {
  setTimeout(() => window.dispatchEvent(new Event('resize')), 100);
}
@@ -108,11 +134,11 @@
    await document.exitFullscreen();
    isFullscreen.value = false;
  }
  setTimeout(resizeAllCharts, 400);
  setTimeout(() => { layoutFill(); resizeAllCharts(); }, 400);
}
function onFullscreenChange() {
  isFullscreen.value = !!document.fullscreenElement;
  setTimeout(resizeAllCharts, 300);
  setTimeout(() => { layoutFill(); resizeAllCharts(); }, 300);
}
// ======== KPI 数字动画 ========
@@ -210,6 +236,7 @@
onMounted(() => {
  document.addEventListener('fullscreenchange', onFullscreenChange);
  setupResizeObserver();
  window.addEventListener('resize', layoutFill);
  loadData();
});
onBeforeUnmount(() => {
@@ -217,6 +244,7 @@
  requestSequence++;
  if (dataRefreshTimer) clearInterval(dataRefreshTimer);
  document.removeEventListener('fullscreenchange', onFullscreenChange);
  window.removeEventListener('resize', layoutFill);
  resizeObserver?.disconnect();
});
</script>
@@ -633,8 +661,10 @@
/* 图表区吃掉剩余高度,行高均分,避免底部大片留白 */
.is-fullscreen > .chart-row {
  flex: 1 0 auto;
  grid-auto-rows: minmax(240px, 1fr);
  flex: 0 0 auto;
  /* 高度由 layoutFill() 量测后写入,保证正好填满视口 */
  height: var(--fill-h, auto);
  grid-auto-rows: minmax(200px, 1fr);
  align-content: stretch;
}