aaa
import RecordCountDisplay from '@/components/RecordCountDisplay';
import { useTableScrollY } from '@/hooks/useTableScrollY';
import { Alert, Button, Card, Flex } from 'antd';
import dayjs from 'dayjs';
import { useCallback, useMemo, useState } from 'react';
import ListModeSelector from './components/ListModeSelectorCopy';
import ProposalSearchPanel from './components/ProposalSearchCopy';
import TableForProject from './components/TableForProject';
import { PROPOSAL_LIST } from './constantsCopy';
// 当月 表示样式
const CURRENT_MONTH_BG = '#f0f7ff'; // 淡蓝色背景
const CURRENT_MONTH_COLOR = '#1677ff'; // Antd 主题蓝
const MonthHeaderLabel: React.FC<{ monthStr: string }> = ({ monthStr }) => {
// 将 "2026/10" 或 "2026-10" 统一转换为 dayjs 对象进行安全比对
const isCurrentMonth = dayjs(monthStr).isSame(dayjs(), 'month');
if (!isCurrentMonth) {
return <>{monthStr}</>;
}
return (
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontWeight: 600 }}>{monthStr}</span>
<span
style={{
fontSize: 10,
lineHeight: '16px',
padding: '0 6px',
color: CURRENT_MONTH_COLOR,
background: CURRENT_MONTH_BG,
borderRadius: 8,
fontWeight: 400,
}}
>
当月
</span>
</span>
);
};
const formatTreeData = (list: any[]): any[] => {
if (!list || !Array.isArray(list)) return [];
return list.map((row, index) => {
// 1. 浅拷贝基础字段
const formattedRow = { ...row };
// 🛠️ 核心修复点 1:为第一层合计父行,生成唯一的 key。例如 "parent_1"
// 这样就算不同大组的 sssAccountID 重复,加上前缀也绝对不会撞车!
formattedRow.key = `parent_${row.sssAccountID || index}`;
// 2. 核心转化:遍历当前行的 dataList 数组
if (row.dataList && Array.isArray(row.dataList)) {
row.dataList.forEach((dataItem: any) => {
if (dataItem.monthly) {
const monthKey = `monthly_${dataItem.monthly}`;
formattedRow[monthKey] = { ...dataItem };
}
});
}
// 3. ✨ 核心修复点 2:为子明细行注入【绝不撞车】的独立 key 身份证!
if (row.children && Array.isArray(row.children)) {
formattedRow.children = row.children.map((child, cIdx) => {
const formattedChild = { ...child };
// 🎯 降维打击:子行的 key 拼装为 "child_父行ID_子行索引"(例如 "child_1_0", "child_1_1")
// 这样它们的 key 就绝对变成了全网唯一,跟父行的 'parent_1' 彻底绝缘!
formattedChild.key = `child_${row.sssAccountID || index}_${cIdx}`;
// 同时把子项里的 dataList 数组也洗一遍
if (child.dataList && Array.isArray(child.dataList)) {
child.dataList.forEach((dataItem: any) => {
if (dataItem.monthly) {
const monthKey = `monthly_${dataItem.monthly}`;
formattedChild[monthKey] = { ...dataItem };
}
});
}
return formattedChild;
});
} else {
formattedRow.children = undefined; // 抹平空子行
}
return formattedRow;
});
};
const ProposalList: React.FC = () => {
const { containerRef, tableContainerRef } = useTableScrollY();
const [selectDate, setSelectDate] = useState(['2026-09']);
const [loading, setLoading] = useState(false);
const [data, setData] = useState([]);
const [currentMode, setCurrentMode] = useState('');
const [layout, setLayout] = useState('project');
const handleSearch = (nextDate) => {
console.log('111111', nextDate);
setSelectDate(nextDate);
return fetchData(nextDate);
};
const fetchData = useCallback(async ({ nextDate }) => {
setLoading(true);
const params: any = {
selectDate: nextDate,
};
console.log(params);
// const res = await getProjectList(params);
// console.log('res', res);
// const dataResponse: any = [
// {
// sssAccountID: '1',
// sssDept: 'DX推進室',
// sssTeam: 'チームA',
// secDept: '第一開発',
// secTeam: 'チーム1',
// dlDept: '大連3部',
// dlRole: 'PM',
// vendonContractName: 'ベンダ契約-0015',
// secContractName: '証券契約-0015',
// dlContractName: '直接契約',
// hasTotal: true,
// dataList: [
// {
// monthly: '202609',
// holidayRate: '100',
// securityRate: '333',
// memo: '1111',
// },
// {
// monthly: '202610',
// holidayRate: '200',
// securityRate: '444',
// memo: '2222',
// },
// ],
// },
// {
// sssAccountID: '2',
// sssDept: 'DX推進室',
// sssTeam: 'チームA',
// secDept: '第一開発',
// secTeam: 'チーム1',
// dlDept: '大連3部',
// dlRole: 'PM',
// vendonContractName: 'ベンダ契約-0015',
// secContractName: '証券契約-0015',
// dlContractName: '直接契約',
// dataList: [
// {
// monthly: '202609',
// holidayRate: '100',
// securityRate: '333',
// memo: '1111',
// },
// ],
// },
// {
// sssAccountID: '2',
// sssDept: '第一開発',
// sssTeam: 'チームB',
// secDept: 'インフラ部',
// secTeam: 'チーム2',
// dlDept: '大連1部',
// dlRole: 'PL',
// vendonContractName: 'ベンダ契約-0001',
// secContractName: '証券契約-0001',
// dlContractName: '直接契約',
// dataList: [
// {
// monthly: '202610',
// holidayRate: '200',
// securityRate: '444',
// memo: '2222',
// },
// ],
// },
// ];
const dataResponse: any = [
{
sssAccountID: '1',
sssDept: 'DX推進室',
sssTeam: 'チームA',
secDept: '第一開発',
secTeam: 'チーム1',
dlDept: '大連3部',
dlRole: 'PM',
vendonContractName: 'ベンダ契約-0015',
secContractName: '証券契約-0015',
dlContractName: '直接契約',
hasTotal: true,
dataList: [
{
monthly: '202607',
holidayRate: '1.0',
securityRate: '333',
memo: '1111',
},
{
monthly: '202608',
holidayRate: '1.0',
securityRate: '444',
memo: '2222',
},
{
monthly: '202609',
holidayRate: '1.0',
securityRate: '444',
memo: '2222',
},
],
children: [
{
sssAccountID: '1',
sssDept: 'DX推進室',
sssTeam: 'チームA',
secDept: '第一開発',
secTeam: 'チーム1',
dlDept: '大連3部',
dlRole: 'PM',
vendonContractName: 'ベンダ契約-0015',
secContractName: '証券契約-0015',
dlContractName: '直接契約',
hasTotal: false,
dataList: [
{
monthly: '202607',
holidayRate: '0.3',
securityRate: '333',
memo: '1111',
},
{
monthly: '202608',
holidayRate: '0.4',
securityRate: '444',
memo: '2222',
},
{
monthly: '202609',
holidayRate: '1.0',
securityRate: '444',
memo: '2222',
},
],
},
{
sssAccountID: '1',
sssDept: 'DX推進室',
sssTeam: 'チームB',
secDept: '第一開発',
secTeam: 'チーム1',
dlDept: '大連3部',
dlRole: 'PM',
vendonContractName: 'ベンダ契約-0015',
secContractName: '証券契約-0015',
dlContractName: '直接契約',
hasTotal: false,
dataList: [
{
monthly: '202607',
holidayRate: '0.7',
securityRate: '333',
memo: '1111',
},
{
monthly: '202608',
holidayRate: '0.6',
securityRate: '444',
memo: '2222',
},
{
monthly: '202609',
holidayRate: '0',
securityRate: '444',
memo: '2222',
},
],
},
],
},
{
sssAccountID: '2',
sssDept: 'DX推進室',
sssTeam: 'チームA',
secDept: '第一開発',
secTeam: 'チーム1',
dlDept: '大連3部',
dlRole: 'PM',
vendonContractName: 'ベンダ契約-0015',
secContractName: '証券契約-0015',
dlContractName: '直接契約',
hasTotal: false,
dataList: [
{
monthly: '202607',
holidayRate: '2.0',
securityRate: '333',
memo: '1111',
},
{
monthly: '202608',
holidayRate: '3.0',
securityRate: '444',
memo: '2222',
},
{
monthly: '202609',
holidayRate: '4.0',
securityRate: '444',
memo: '2222',
},
],
children: [],
},
// {
// sssAccountID: '1',
// sssDept: 'DX推進室',
// sssTeam: 'チームA',
// secDept: '第一開発',
// secTeam: 'チーム1',
// dlDept: '大連3部',
// dlRole: 'PM',
// vendonContractName: 'ベンダ契約-0015',
// secContractName: '証券契約-0015',
// dlContractName: '直接契約',
// hasTotal: false,
// dataList: [
// {
// monthly: '202607',
// holidayRate: '0.3',
// securityRate: '333',
// memo: '1111',
// },
// {
// monthly: '202608',
// holidayRate: '0.4',
// securityRate: '444',
// memo: '2222',
// },
// {
// monthly: '202609',
// holidayRate: '1.0',
// securityRate: '444',
// memo: '2222',
// },
// ],
// },
// {
// sssAccountID: '1',
// sssDept: 'DX推進室',
// sssTeam: 'チームB',
// secDept: '第一開発',
// secTeam: 'チーム1',
// dlDept: '大連3部',
// dlRole: 'PM',
// vendonContractName: 'ベンダ契約-0015',
// secContractName: '証券契約-0015',
// dlContractName: '直接契約',
// hasTotal: false,
// dataList: [
// {
// monthly: '202607',
// holidayRate: '0.7',
// securityRate: '333',
// memo: '1111',
// },
// {
// monthly: '202608',
// holidayRate: '0.6',
// securityRate: '444',
// memo: '2222',
// },
// {
// monthly: '202609',
// holidayRate: '0',
// securityRate: '444',
// memo: '2222',
// },
// ],
// },
// {
// sssAccountID: '2',
// sssDept: 'DX推進室',
// sssTeam: 'チームA',
// secDept: '第一開発',
// secTeam: 'チーム1',
// dlDept: '大連3部',
// dlRole: 'PM',
// vendonContractName: 'ベンダ契約-0015',
// secContractName: '証券契約-0015',
// dlContractName: '直接契約',
// dataList: [
// {
// monthly: '202609',
// holidayRate: '100',
// securityRate: '333',
// memo: '1111',
// },
// ],
// },
// {
// sssAccountID: '2',
// sssDept: '第一開発',
// sssTeam: 'チームB',
// secDept: 'インフラ部',
// secTeam: 'チーム2',
// dlDept: '大連1部',
// dlRole: 'PL',
// vendonContractName: 'ベンダ契約-0001',
// secContractName: '証券契約-0001',
// dlContractName: '直接契約',
// dataList: [
// {
// monthly: '202610',
// holidayRate: '200',
// securityRate: '444',
// memo: '2222',
// },
// ],
// },
];
const formatData: any = formatTreeData(dataResponse);
setData(formatData);
// setData(dataResponse);
setLoading(false);
}, []);
const [expandedKeys, setExpandedKeys] = useState<React.Key[]>([]);
const handleModeChange = useCallback((value: string) => {
console.log('选择哪个Mode了', value);
setCurrentMode(value);
}, []);
const handleLayoutChange = useCallback((value: string) => {
console.log('选择哪个Layout了', value);
setLayout(value);
}, []);
// // TODO: 列权限 控制
const getMonthsInRange = (selectDate) => {
if (!selectDate || !selectDate[0] || !selectDate[1]) {
const currentMonth = dayjs().format('YYYY/MM'); // 例如今天的日期会生成 "2026/10"
return [currentMonth];
}
const start = dayjs(selectDate?.[0]);
const end = dayjs(selectDate?.[1]);
const months: string[] = [];
let current = start;
// 生成期间的每一个月份
while (current.isBefore(end) || current.isSame(end, 'month')) {
months.push(current.format('YYYY/MM'));
current = current.add(1, 'month'); // 每次增加一个月
}
return months;
};
const generateChildrenColumn = (current, totalDate: string[]) => {
return totalDate.map((item) => {
const isCurrent = dayjs(item).isSame(dayjs(), 'month');
return {
title: <MonthHeaderLabel monthStr={item} />,
dataIndex: [`monthly_${item.replace('/', '')}`, current.itemVar],
align: 'right' as const,
width: 180,
onHeaderCell: () => ({
style: isCurrent ? { backgroundColor: '#fafcff' } : {},
}),
onCell: () => ({
style: isCurrent ? { backgroundColor: '#fafcff' } : {},
}),
};
});
};
// children: [
// {
// title: '2026/09',
// dataIndex: ['monthly_202609', item.key],
// },
// {
// title: '2026/10',
// dataIndex: ['monthly_202610', item.key],
// },
// ],
// 項目軸表示
const buildColumnsForTableProject = (selectDate) => {
console.log('column接收到的日期', selectDate);
const totalDate = getMonthsInRange(selectDate);
console.log('totalDate', totalDate);
return PROPOSAL_LIST.map((current) => {
const hasChildren = current.dynamic && totalDate && totalDate.length > 0;
const baseCol: any = {
title: current.itemName,
...(hasChildren ? {} : { dataIndex: current.itemVar }),
key: current.itemVar,
...(hasChildren ? { children: generateChildrenColumn(current, totalDate) } : {}),
width: 150,
};
if (current.itemName === '合計有無') {
baseCol.render = (hasTotal: boolean, record: any) => {
console.log('recordxxxxxxxxxx', record);
if (!hasTotal) return <span style={{ color: '#bfbfbf' }}>无</span>;
const parentKey = record.key || record.sssAccountID;
const isExpanded = expandedKeys.includes(parentKey);
console.log('isExpanded', isExpanded);
return (
<span
onClick={(e) => {
e.stopPropagation(); // 阻断行点击,防止产生多余副作用
setExpandedKeys((prev) => (isExpanded ? prev.filter((k) => k !== parentKey) : [...prev, parentKey]));
}}
style={{
cursor: 'pointer',
display: 'inline-flex',
alignItems: 'center',
gap: 6,
paddingLeft: 8,
color: '#1677ff',
userSelect: 'none',
}}
>
<span
style={{
fontSize: 12,
transition: 'transform 0.2s',
display: 'inline-block',
transform: isExpanded ? 'rotate(90deg)' : 'none',
}}
>
▶
</span>
<span style={{ fontWeight: 500 }}>有</span>
</span>
);
};
}
return baseCol;
});
};
// 期間軸表示
const buildColumnsForTableDateRange = (selectDate) => {
const totalDate = getMonthsInRange(selectDate); // 这里拿到的 totalDate 绝对是数组,最少也是 []
// const staticColumns = PROPOSAL_LIST.filter((item) => !item.dynamic).map((item) => ({
// title: item.itemName,
// dataIndex: item.itemVar,
// key: item.itemVar,
// width: 120,
// }));
const staticColumns = PROPOSAL_LIST.filter((item) => !item.dynamic).map((item) => {
const baseCol: any = {
title: item.itemName,
dataIndex: item.itemVar,
key: item.itemVar,
width: 120,
};
if (item.itemName === '合計有無') {
baseCol.render = (hasTotal: boolean, record: any) => {
console.log('recordxxxxxxxxxx', record);
if (!hasTotal) return <span style={{ color: '#bfbfbf' }}>无</span>;
const parentKey = record.key || record.sssAccountID;
const isExpanded = expandedKeys.includes(parentKey);
console.log('isExpanded', isExpanded);
return (
<span
onClick={(e) => {
e.stopPropagation(); // 阻断行点击,防止产生多余副作用
setExpandedKeys((prev) => (isExpanded ? prev.filter((k) => k !== parentKey) : [...prev, parentKey]));
}}
style={{
cursor: 'pointer',
display: 'inline-flex',
alignItems: 'center',
gap: 6,
color: '#1677ff',
userSelect: 'none',
}}
>
<span
style={{
fontSize: 12,
transition: 'transform 0.2s',
display: 'inline-block',
transform: isExpanded ? 'rotate(90deg)' : 'none',
}}
>
▶
</span>
<span style={{ fontWeight: 500 }}>有</span>
</span>
);
};
}
return baseCol;
});
const dynamicItems = PROPOSAL_LIST.filter((item) => item.dynamic);
const monthGroups = totalDate.map((month) => {
const isCurrent = dayjs(month).isSame(dayjs(), 'month');
console.log('isCurrent', isCurrent);
return {
title: <MonthHeaderLabel monthStr={month} />,
key: `group-${month}`,
align: 'center' as const,
onHeaderCell: () => ({
style: isCurrent ? { backgroundColor: '#fafcff' } : {},
}),
children: dynamicItems.map((item) => {
const cleanMonth = month.replace('/', '');
const monthKey = `monthly_${cleanMonth}`;
return {
title: item.itemName,
key: `${monthKey}__${item.itemVar}`,
dataIndex: [monthKey, item.itemVar],
align: 'left' as const,
width: 180,
onHeaderCell: () => {
return {
style: isCurrent ? { backgroundColor: '#fafcff' } : {},
};
},
onCell: () => {
return {
style: isCurrent ? { backgroundColor: '#fafcff' } : {},
};
},
};
}),
};
});
return [...staticColumns, ...monthGroups];
};
const columnsForProject = useMemo(() => {
return buildColumnsForTableProject(selectDate);
}, [selectDate, expandedKeys, setExpandedKeys]);
console.log('columnsForProject', columnsForProject);
const columnsForDateRange = useMemo(() => {
return buildColumnsForTableDateRange(selectDate);
}, [selectDate, expandedKeys, setExpandedKeys]);
console.log('columnsForDateRange', columnsForDateRange);
return (
<div ref={containerRef} className="scroll-card-container">
<Card>
<div>
<ProposalSearchPanel onSearch={handleSearch} />
<Alert
description="対象月が未選択のため、当月(2026/09)の情報を表示しています。"
type="info"
showIcon
banner
style={{ padding: '5px 12px' }}
/>
</div>
<Flex justify="space-between" className="mt-5 mb-0 h-8">
<ListModeSelector onModeChange={handleModeChange} onLayoutChange={handleLayoutChange}></ListModeSelector>
<Flex gap="small">
<Button type="primary" size="small">
追加
</Button>
</Flex>
</Flex>
<div className="ml-2">
<RecordCountDisplay count={100} />
</div>
<div ref={tableContainerRef} className="scroll-table-container">
{/* {layout === 'project' ? (
<TableForProject
selectDate={selectDate}
loading={loading}
resData={data}
currentMode={currentMode}
columns={columns}
></TableForProject>
) : (
<TableForDateRange columns={columnsForDateRange}></TableForDateRange>
)} */}
<TableForProject
selectDate={selectDate}
loading={loading}
resData={data}
currentMode={currentMode}
columns={layout === 'project' ? columnsForProject : columnsForDateRange}
expandedKeys={expandedKeys}
setExpandedKeys={setExpandedKeys}
></TableForProject>
</div>
</Card>
</div>
);
};
export default ProposalList;

浙公网安备 33010602011771号