table for change

import type { TableColumnsType, FilterDropdownProps } from 'antd';

// ============ 类型 ============
type ProposalData = any;
type ColConfig = {
  key: string;
  title: string;
  dataIndex?: string | string[];
  monthly?: boolean;
  numeric?: boolean;
};

type CommonBuilderParams = {
  visibleCols: ColConfig[];
  hasData: boolean;
  leftFixedKey?: string[];
  rightFixedKey?: string[];
  currentMonthKey?: string[];
  CURRENT_MONTH: string;
  dataSource: ProposalData[];
  renderOperation: (...args: any[]) => React.ReactNode;
  calcTextWidth: (txt: string) => number;
  localeCompareJa: (a: string, b: string) => number;
  monthLabel: (v: string) => string;
  generateColumnFilters: (data: ProposalData[], dataIndex: string | string[]) => any[];
  matchColumnFilter: (val: any[], recordField: any) => boolean;
  VirtualFilterDropdown: React.ComponentType<FilterDropdownProps>;
};

// ============ 公共小工具(全部复用) ============
/** 静态列 */
function buildStaticCol(params: {
  col: ColConfig;
  fixed: 'left' | 'right' | undefined;
  common: CommonBuilderParams;
}) {
  const { col, fixed, common } = params;
  const { hasData, dataSource, calcTextWidth, localeCompareJa, generateColumnFilters, matchColumnFilter, VirtualFilterDropdown } = common;
  const dataIndex = col.dataIndex as keyof ProposalData;

  const headerWidth = calcTextWidth(col.title);
  const dataWidth = dataSource.reduce((max, row) => Math.max(max, calcTextWidth(String(row[dataIndex] ?? ''))), 0);
  const colWidth = Math.max(headerWidth, dataWidth) + 56;

  return {
    title: col.title,
    dataIndex: col.dataIndex,
    key: col.key,
    width: colWidth,
    ...(fixed ? { fixed } : {}),
    onCell: () => ({ style: { whiteSpace: 'nowrap' } }),
    onHeaderCell: () => ({ style: { whiteSpace: 'nowrap' } }),
    sorter: hasData
      ? (a: ProposalData, b: ProposalData) =>
          localeCompareJa(String(a[dataIndex] ?? ''), String(b[dataIndex] ?? ''))
      : (false as const),
    ...(hasData
      ? {
          filterDropdown: (props: FilterDropdownProps) => (
            <VirtualFilterDropdown {...props} filters={generateColumnFilters(dataSource, dataIndex)} />
          ),
          onFilter: (value: React.Key | boolean, record: ProposalData) =>
            matchColumnFilter(record[dataIndex], [value as React.Key]),
        }
      : {}),
  };
}

/** 当月表头badge */
function MonthHeaderLabel({ m, isCurrent, monthLabel }: { m: string; isCurrent: boolean; monthLabel: (v: string) => string }) {
  if (!isCurrent) return monthLabel(m);
  return (
    <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
      <span style={{ fontWeight: 600 }}>{monthLabel(m)}</span>
      <span
        style={{
          fontSize: 10,
          lineHeight: '16px',
          padding: '0 6px',
          color: '#1677ff',
          background: '#f0f7ff',
          borderRadius: 8,
          fontWeight: 400,
        }}
      >
        当月
      </span>
    </span>
  );
}

/** 【公共】生成operation操作列 */
function getOperationColumn(renderOperation: CommonBuilderParams['renderOperation']) {
  return {
    title: '操作',
    key: 'operation',
    width: 57,
    fixed: 'right' as const,
    align: 'center' as const,
    onCell: () => ({ style: { textAlign: 'center' as const } }),
    onHeaderCell: () => ({ style: { textAlign: 'center' as const } }),
    render: renderOperation,
  };
}

/** 【公共】生成单个月度子单元格列(period / item 都调用) */
function buildMonthChildCol(params: {
  col: ColConfig;
  m: string;
  isCurrent: boolean;
  common: CommonBuilderParams;
  extraWidth?: number; // item模式需要额外宽度偏移
}) {
  const { col, m, isCurrent, common, extraWidth = 0 } = params;
  const { hasData, dataSource, calcTextWidth, localeCompareJa, rightFixedKey } = common;
  const monthKey = `${col.key}__${m}`;
  const isNumeric = col.numeric;

  const headerWidth = calcTextWidth(col.title);
  const dataWidth = dataSource.reduce(
    (max, row) => Math.max(max, calcTextWidth(String(row.monthly?.[monthKey] ?? ''))),
    0
  );
  const colWidth = Math.max(headerWidth, dataWidth) + 40 + extraWidth;
  const isRightFixed = !!rightFixedKey?.includes(col.key);

  return {
    title: <MonthHeaderLabel m={m} isCurrent={isCurrent} monthLabel={common.monthLabel} />,
    key: monthKey,
    dataIndex: ['monthly', monthKey],
    width: colWidth,
    fixed: isRightFixed ? ('right' as const) : undefined,
    align: isNumeric ? ('right' as const) : undefined,
    onCell: () => ({
      style: {
        whiteSpace: 'nowrap',
        ...(isNumeric ? { textAlign: 'right' as const } : {}),
        ...(isCurrent ? { backgroundColor: '#fafcff' } : {}),
      },
    }),
    onHeaderCell: () => ({
      style: { whiteSpace: 'nowrap', ...(isCurrent ? { backgroundColor: '#fafcff' } : {}) },
    }),
    render: (_: unknown, record: ProposalData) => record.monthly?.[monthKey] ?? '',
    sorter: hasData
      ? (a: ProposalData, b: ProposalData) =>
          localeCompareJa(String(a.monthly?.[monthKey] ?? ''), String(b.monthly?.[monthKey] ?? ''))
      : false,
  };
}

// ============ 期間軸:月份作为父分组,指标作为子列 ============
function buildPeriodColumns(common: CommonBuilderParams, months: string[]) {
  const { visibleCols, leftFixedKey, rightFixedKey, currentMonthKey, CURRENT_MONTH } = common;
  const staticVisibleCols = visibleCols.filter((col) => !col.monthly);
  const monthlyVisibleCols = visibleCols.filter((col) => col.monthly);

  const leftStaticCols: TableColumnsType<ProposalData> = [];
  const normalStaticCols: TableColumnsType<ProposalData> = [];
  staticVisibleCols.forEach((col) => {
    if (leftFixedKey?.includes(col.key)) {
      leftStaticCols.push(buildStaticCol({ col, fixed: 'left', common }));
    } else {
      normalStaticCols.push(buildStaticCol({ col, fixed: undefined, common }));
    }
  });

  // 构建月份父分组
  const monthGroups = months.map((m) => {
    const isCurrent = m === CURRENT_MONTH;
    const childrenCols = monthlyVisibleCols
      .filter((col) => !currentMonthKey?.includes(col.key))
      .map((col) => buildMonthChildCol({ col, m, isCurrent, common }));

    return {
      title: common.monthLabel(m),
      key: `group_${m}`,
      onHeaderCell: () => ({ style: { ...(isCurrent ? { backgroundColor: '#fafcff' } : {}) } }),
      children: childrenCols,
    };
  });

  const currentMonthGroups =
    currentMonthKey && currentMonthKey.length > 0
      ? [CURRENT_MONTH].map((m) => {
          const isCurrent = m === CURRENT_MONTH;
          const childrenCols = monthlyVisibleCols
            .filter((col) => currentMonthKey?.includes(col.key))
            .map((col) => buildMonthChildCol({ col, m, isCurrent, common }));

          return {
            title: common.monthLabel(m),
            key: `group_${m}`,
            onHeaderCell: () => ({ style: { ...(isCurrent ? { backgroundColor: '#fafcff' } : {}) } }),
            children: childrenCols,
          };
        })
      : [];

  // 方案B:剥离右固定子列,拆分分组,过滤空分组
  const rightItemsCols =
    rightFixedKey && rightFixedKey.length > 0
      ? monthGroups
          .map((item) => ({
            ...item,
            children: item.children.filter((child) => child.fixed === 'right'),
          }))
          .filter((g) => g.children.length > 0)
      : [];

  const monthlyItemsColsWithoutRight = monthGroups.map((item) => ({
    ...item,
    children: item.children.filter((child) => !child.fixed),
  }));

  const operationCol = getOperationColumn(common.renderOperation);

  return [
    ...leftStaticCols,
    ...normalStaticCols,
    ...currentMonthGroups,
    ...monthlyItemsColsWithoutRight,
    ...rightItemsCols,
    operationCol,
  ];
}

// ============ 項目軸:指标作为父分组,月份作为子列 ============
function buildItemColumns(common: CommonBuilderParams, effectiveMonths: string[]) {
  const { visibleCols, leftFixedKey, rightFixedKey, currentMonthKey, CURRENT_MONTH } = common;

  const leftItemCols: TableColumnsType<ProposalData> = [];
  const normalStaticItemCols: TableColumnsType<ProposalData> = [];
  const monthlyItemCols: TableColumnsType<ProposalData> = [];

  visibleCols.forEach((col) => {
    if (col.monthly) {
      const subMonths = currentMonthKey?.includes(col.key) ? [CURRENT_MONTH] : effectiveMonths;
      monthlyItemCols.push({
        title: col.title,
        key: col.key,
        fixed: rightFixedKey?.includes(col.key) ? 'right' : undefined,
        children: subMonths.map((m) => {
          // item模式宽度需要额外偏移,传extraWidth
          return buildMonthChildCol({ col, m, isCurrent: m === CURRENT_MONTH, common, extraWidth: 44 });
        }),
      });
      return;
    }

    if (leftFixedKey?.includes(col.key)) {
      leftItemCols.push(buildStaticCol({ col, fixed: 'left', common }));
    } else {
      normalStaticItemCols.push(buildStaticCol({ col, fixed: undefined, common }));
    }
  });

  const operationCol = getOperationColumn(common.renderOperation);

  const currentMonthCols = monthlyItemCols.filter((item) => currentMonthKey?.includes(item.key as string));
  const totalMonthCols = monthlyItemCols.filter(
    (item) => !currentMonthKey?.includes(item.key as string) && !item.fixed
  );
  const rightItemsCols = monthlyItemCols.filter((item) => item.fixed === 'right') ?? [];

  return [
    ...leftItemCols,
    ...normalStaticItemCols,
    ...currentMonthCols,
    ...totalMonthCols,
    ...rightItemsCols,
    operationCol,
  ];
}

  

const columns = useMemo<TableColumnsType<ProposalData>>(() => {
  const visibleCols = PROPOSAL_COLUMNS.filter((col) => visibleColumnKeys.includes(col.key));
  const hasData = dataSource.length > 0;

  const common: CommonBuilderParams = {
    visibleCols,
    hasData,
    leftFixedKey,
    rightFixedKey,
    currentMonthKey,
    CURRENT_MONTH,
    dataSource,
    renderOperation,
    calcTextWidth,
    localeCompareJa,
    monthLabel,
    generateColumnFilters,
    matchColumnFilter,
    VirtualFilterDropdown,
  };

  if (layout === 'period') {
    return buildPeriodColumns(common, effectiveMonths);
  } else {
    return buildItemColumns(common, effectiveMonths);
  }
}, [
  visibleColumnKeys,
  leftFixedKey,
  rightFixedKey,
  currentMonthKey,
  dataSource,
  effectiveMonths,
  layout,
  renderOperation,
  PROPOSAL_COLUMNS,
  CURRENT_MONTH,
  calcTextWidth,
  localeCompareJa,
  generateColumnFilters,
  matchColumnFilter,
  monthLabel,
]);

  

posted @ 2026-10-15 09:48  小兔子09  阅读(3)  评论(0)    收藏  举报