组件外

// ============ 组件外:类型补充 ============
type CommonBuilderParams = {
  visibleCols: ColConfig[];
  hasData: boolean;
  leftFixedKey?: string[];
  rightFixedKey?: string[];
  currentMonthKey?: string[];
  CURRENT_MONTH: string;
  dataSource: ProposalData[];
  renderOperation: CommonBuilderParams['renderOperation']; // 你原来的类型
  calcTextWidth: (v: string) => number;
  monthLabel: (v: string) => string;
  matchColumnFilter: (v: unknown, keys: React.Key[]) => boolean;
  layout: 'period' | 'item'; // 新增
};

const buildStaticCol = (params: {
  col: ColConfig;
  fixed: 'left' | 'right' | undefined;
  common: CommonBuilderParams;
}) => {
  const { col, fixed, common } = params;
  const { hasData, dataSource, calcTextWidth } = 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]),
        }
      : {}),
  };
};


// ============ 组件外:buildMonthChildCol ============
const buildMonthChildCol = (params: {
  col: ColConfig;
  m: string;
  isCurrent: boolean;
  common: CommonBuilderParams;
  extraWidth?: number;
}) => {
  const { col, m, isCurrent, common, extraWidth = 0 } = params;
  const { hasData, dataSource, calcTextWidth, rightFixedKey, layout } = 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);

  const displayTitle =
    layout === 'period'
      ? col.title
      : <MonthHeaderLabel m={m} isCurrent={isCurrent} monthLabel={common.monthLabel} />;

  return {
    title: displayTitle,
    key: monthKey,
    dataIndex: ['monthly', monthKey],
    width: colWidth,
    fixed: isRightFixed ? ('right' as const) : undefined,
    align: isNumeric ? ('right' as const) : undefined,
    onCell: () => ({
      style: {
        whiteSpace: 'nowrap' as const,
        ...(isNumeric ? { textAlign: 'right' as const } : {}),
        ...(isCurrent ? { backgroundColor: '#fafcff' } : {}),
      },
    }),
    onHeaderCell: () => ({
      style: { whiteSpace: 'nowrap' as const, ...(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 as const),
  };
};

// ============ 组件外:buildItemColumns ============
const 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) =>
          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,
  ];
};


// ============ 组件外:buildPeriodColumns ============
const 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 hasCurrentGroup = !!currentMonthKey && currentMonthKey.length > 0;

  // 去掉和 currentMonthGroup 重叠的月份,避免重复 group key
  const normalMonths = hasCurrentGroup ? months.filter((m) => m !== CURRENT_MONTH) : months;

  const monthGroups = normalMonths.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: <MonthHeaderLabel m={m} isCurrent={isCurrent} monthLabel={common.monthLabel} />,
      key: `group_${m}`,
      onHeaderCell: () => ({ style: { ...(isCurrent ? { backgroundColor: '#fafcff' } : {}) } }),
      children: childrenCols,
    };
  });

  const currentMonthGroups = hasCurrentGroup
    ? [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: <MonthHeaderLabel m={m} isCurrent={isCurrent} monthLabel={common.monthLabel} />,
          key: `group_current_${m}`, // 用不同前缀,彻底避免 key 冲突
          onHeaderCell: () => ({ style: { ...(isCurrent ? { backgroundColor: '#fafcff' } : {}) } }),
          children: childrenCols,
        };
      })
    : [];

  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,
  ];
};

// ============ 组件内:只留 useMemo ============
const ProposalList: React.FC = () => {
  // ...其他 hooks / props 不变

  const columns = useMemo<TableColumnsType<ProposalData>>(() => {
    const targetKeys =
      visibleColumnKeys && visibleColumnKeys.length > 0
        ? visibleColumnKeys
        : PROPOSAL_COLUMNS.map((opt) => opt.key);

    const visibleCols = targetKeys
      .map((key) => PROPOSAL_COLUMNS.find((col) => col.key === key))
      .filter(Boolean) as ColConfig[];

    const hasData = dataSource.length > 0;

    const common: CommonBuilderParams = {
      visibleCols,
      hasData,
      leftFixedKey,
      rightFixedKey,
      currentMonthKey,
      CURRENT_MONTH,
      dataSource,
      renderOperation,
      calcTextWidth,
      monthLabel,
      matchColumnFilter,
      layout, // 新增
    };

    return layout === 'period'
      ? buildPeriodColumns(common, effectiveMonths)
      : buildItemColumns(common, effectiveMonths);
  }, [
    visibleColumnKeys,
    leftFixedKey,
    rightFixedKey,
    currentMonthKey,
    dataSource,
    effectiveMonths,
    layout,
    renderOperation,
    PROPOSAL_COLUMNS,
    CURRENT_MONTH,
    calcTextWidth,
    matchColumnFilter,
    monthLabel,
  ]);

  // ...
};
posted @ 2026-10-15 13:42  小兔子09  阅读(5)  评论(0)    收藏  举报