てst

下面给出修改后的版本。核心改动:

  1. 去掉 dataIndex 与 render 的重复(保留 render,移除嵌套 dataIndex)。

  2. 项目轴布局中,把 fixed: 'right' 从分组列下移到叶子列(buildMonthChildCol 已经支持)。

  3. 期间轴布局中,右侧分组的 key 加上 _right 后缀,避免 React key 冲突。

  4. currentMonthGroups 改用条件返回单个分组,不再用单元素 map。

  5. “当月”徽标宽度显式计入表头宽度,不再依赖调用方传的魔法 extraWidth。

  6. 给 currentMonthKey 的两种语义加注释。

const columns = useMemo<TableColumnsType<ProposalData>>(() => {
// 全角文字を考慮した文字幅の概算(px)
const calcTextWidth = (text: string): number => {
let w = 0;
for (const ch of text) {
w += ch.charCodeAt(0) > 255 ? 16 : 8;
}
return w;
};

const monthLabel = (m: string) => `${m.slice(0, 4)}/${m.slice(4)}`;
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 IColConfig[];

const hasData = dataSource.length > 0;

const common: ICommonBuilderParams = {
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,
]);

// --- 组件外 ---

// 「当月」バッジの概算幅(px)。スタイル変更時はここを更新する。
const CURRENT_MONTH_BADGE_WIDTH = 56; // バッジ + gap の合計

const MonthHeaderLabel: React.FC<{ m: string; isCurrent: boolean; monthLabel: (v: string) => string }> = ({
m,
isCurrent,
monthLabel,
}) => {
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>
);
};

const getOperationColumn = (renderOperation: ICommonBuilderParams['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,
};
};

// 基本情報
const buildStaticCol = (params: {
col: IColConfig;
fixed: 'left' | 'right' | undefined;
common: ICommonBuilderParams;
}) => {
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]),
}
: {}),
};
};

// 月次情報
const buildMonthChildCol = (params: {
col: IColConfig;
m: string;
isCurrent: boolean;
common: ICommonBuilderParams;
}) => {
const { col, m, isCurrent, common } = params;
const { hasData, dataSource, calcTextWidth, rightFixedKey, layout } = common;
const monthKey = `${col.key}__${m}`;
const isNumeric = col.numeric;

// 期間軸ではタイトルが col.title、項目軸では <MonthHeaderLabel> が描画される。
// 当月バッジ分の幅を表頭幅に明示的に加算する。
const headerWidth =
layout === 'period'
? calcTextWidth(col.title)
: calcTextWidth(col.title) + (isCurrent ? CURRENT_MONTH_BADGE_WIDTH : 0);
const dataWidth = dataSource.reduce(
(max, row) => Math.max(max, calcTextWidth(String(row.monthly?.[monthKey] ?? ''))),
0
);
const colWidth = Math.max(headerWidth, dataWidth) + 40;
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,
// render で値を取り出すため dataIndex は不要
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,
};
};

// 構築項目軸
const buildItemColumns = (common: ICommonBuilderParams, 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) => {
// --- 月次項目:選択された月ごとにサブ列を展開 ---
// currentMonthKey に含まれる列は「当月のみ」表示する。
if (col.monthly) {
const subMonths = currentMonthKey?.includes(col.key) ? [CURRENT_MONTH] : effectiveMonths;
// fixed は葉列(buildMonthChildCol 内)で設定する。グループ列には付けない。
monthlyItemCols.push({
title: col.title,
key: col.key,
children: subMonths.map((m) => buildMonthChildCol({ col, m, isCurrent: m === CURRENT_MONTH, common })),
});
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 buildPeriodColumns = (common: ICommonBuilderParams, 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: <MonthHeaderLabel m={m} isCurrent={isCurrent} monthLabel={common.monthLabel} />,
key: `group_${m}`,
onHeaderCell: () => ({ style: { ...(isCurrent ? { backgroundColor: '#fafcff' } : {}) } }),
children: childrenCols,
};
});

// currentMonthKey に含まれる列は、当月グループとして独立させる。
const currentMonthGroups =
currentMonthKey && currentMonthKey.length > 0
? [
{
title: <MonthHeaderLabel m={CURRENT_MONTH} isCurrent monthLabel={common.monthLabel} />,
key: `group_${CURRENT_MONTH}`,
onHeaderCell: () => ({ style: { backgroundColor: '#fafcff' } }),
children: monthlyVisibleCols
.filter((col) => currentMonthKey.includes(col.key))
.map((col) => buildMonthChildCol({ col, m: CURRENT_MONTH, isCurrent: true, common })),
},
]
: [];

// 右固定の月次列は、月ごとの右固定グループとして切り出す。
// key は左側グループと衝突しないよう _right を付与する。
const rightItemsCols =
rightFixedKey && rightFixedKey.length > 0
? monthGroups
.map((item) => ({
...item,
key: `${item.key}_right`,
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,
];
};

 

改动要点回顾

  • buildMonthChildCol:删除 dataIndex(用 render 取值),移除 extraWidth 参数,改为根据 layout 和 isCurrent 显式计算表头宽度,并引入常量 CURRENT_MONTH_BADGE_WIDTH。

  • buildItemColumns:分组列不再设 fixed,只由叶子列(buildMonthChildCol 内根据 rightFixedKey)决定。这样右固定的月次列在分组下也能正确固定,且不会和 operationCol 抢位。

  • buildPeriodColumns:右侧分组的 key 加 _right 后缀,消除重复 key;currentMonthGroups 直接用条件构造单个分组,避免单元素 map。

  • 注释:在 buildItemColumns 和 buildPeriodColumns 里分别注明 currentMonthKey 的两种语义,避免后续误改。

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