KpiBoard.tsx 3.32 KB
// REQ-USR-003: KPI 合并网格(D5:CSS Grid gridRow span 复刻原型 kpi-body 合并)。
// 「导航类型」整列一格跨全部行;「角色」「子流程」按 roleSpan/subSpan 合并;空数据 → Empty。
import { Empty } from 'antd';
import type { KpiRow } from './dashboardData';
import { KPI_HEADERS } from './dashboardData';
import styles from './HomePage.module.css';

interface KpiBoardProps {
  rows: KpiRow[];
  /** KPI 待处理事项/描述为纯展示(蓝色链接样式),点击不跳转;保留占位以示意无导航。 */
  onNavigate?: (target: string) => void;
}

export default function KpiBoard({ rows }: KpiBoardProps) {
  if (!rows.length) {
    return (
      <div className={styles.kpiEmpty} data-testid="kpi-empty">
        <Empty />
      </div>
    );
  }

  const total = rows.length;
  // 表头行 = 第 1 行;数据行从第 2 行起
  const cells: React.ReactNode[] = [];

  // 表头 7 列
  KPI_HEADERS.forEach((h, i) => {
    cells.push(
      <div key={`h-${i}`} className={styles.kpiHeadCell} style={{ gridColumn: i + 1, gridRow: 1 }}>
        {h}
      </div>,
    );
  });

  // 导航类型:整列一格,跨全部数据行(复刻原型 navCell '按角色')
  cells.push(
    <div
      key="navtype"
      className={styles.kpiNavType}
      style={{ gridColumn: 1, gridRow: `2 / span ${total}` }}
    >
      按角色
    </div>,
  );

  rows.forEach((row, idx) => {
    const curRow = idx + 2;
    const alt = idx % 2 === 1 ? styles.kpiRowAlt : '';

    // 角色(带 rowSpan)
    if (row.role) {
      const span = row.roleSpan ?? 1;
      cells.push(
        <div
          key={`role-${idx}`}
          className={`${styles.kpiCellCenter} ${alt}`}
          style={{ gridColumn: 2, gridRow: `${curRow} / span ${span}` }}
        >
          {row.role}
        </div>,
      );
    }

    // KPI 待处理事项(蓝色链接样式,纯展示)
    cells.push(
      <div
        key={`item-${idx}`}
        className={`${styles.kpiLink} ${alt}`}
        style={{ gridColumn: 3, gridRow: curRow }}
      >
        {row.item}
      </div>,
    );

    // KPI 内容描述(蓝色链接样式,纯展示)
    cells.push(
      <div
        key={`desc-${idx}`}
        className={`${styles.kpiLink} ${alt}`}
        style={{ gridColumn: 4, gridRow: curRow }}
      >
        {row.desc}
      </div>,
    );

    // 今日未处理
    cells.push(
      <div
        key={`today-${idx}`}
        className={`${styles.kpiNum} ${row.red ? styles.kpiNumRed : ''} ${alt}`}
        style={{ gridColumn: 5, gridRow: curRow }}
      >
        {row.today}
      </div>,
    );

    // 未清总数
    cells.push(
      <div
        key={`total-${idx}`}
        className={`${styles.kpiNum} ${row.red ? styles.kpiNumRed : ''} ${alt}`}
        style={{ gridColumn: 6, gridRow: curRow }}
      >
        {row.total}
      </div>,
    );

    // 子流程(带 rowSpan)
    if (row.sub && row.subSpan) {
      cells.push(
        <div
          key={`sub-${idx}`}
          className={styles.kpiSubProc}
          style={{ gridColumn: 7, gridRow: `${curRow} / span ${row.subSpan}` }}
        >
          {row.sub}
        </div>,
      );
    }
  });

  return (
    <div
      className={styles.kpiBoard}
      style={{ gridTemplateRows: `38px repeat(${total}, minmax(38px, auto))` }}
    >
      {cells}
    </div>
  );
}