Files
fe_supplier_frontend/src/components/EvaluateTemplateTable/EvaluateTemplateTable.tsx
2025-06-27 17:48:37 +08:00

818 lines
26 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 供应商评价 模板管理新增中的table
import React, { useState, useEffect } from 'react';
import {
Table,
Input,
Button,
Select,
Form,
InputNumber,
message,
Popconfirm,
} from 'antd';
import {
PlusOutlined,
MinusCircleOutlined,
PlusCircleOutlined,
DeleteOutlined,
} from '@ant-design/icons';
import { getDictList, DictItem } from '@/servers/api/dicts';
import { StarLevel, StarLevelText } from '@/dicts/supplierTemplateDict';
import { generateUUID } from '@/utils/utils';
import './EvaluateTemplateTable.less';
const { Option } = Select;
const { TextArea } = Input;
interface EvaluateTemplateTableProps {
value?: any[];
onChange?: (value: any[]) => void;
isDetail?: boolean; // 是否详情展示用如果为true则将input都改为text展示并且将操作列隐藏
isCheck?: boolean; // 是否显示勾选操作如果为true则在表格最后一列增加勾选操作项
onSelect?: (selectedItems: any[]) => void; // 勾选回调函数
defaultSelectedIds?: string[]; // 默认选中的二级指标ID数组
}
// 内部使用的数据结构,扁平化后的行数据
interface TableRowItem {
key: string;
stId?: string; // 一级指标ID
ndId?: string; // 二级指标ID
baseIndicator: string;
descIndicator?: string;
stScore: string;
indicatorType?: string;
subIndicator: string;
ndScore: string;
isStar: string;
desc?: string;
orderBy?: number;
ndOrderBy?: number;
selected?: boolean; // 是否选中
}
const EvaluateTemplateTable: React.FC<EvaluateTemplateTableProps> = ({
value = [],
onChange,
isDetail = false,
isCheck = false,
onSelect,
defaultSelectedIds = [],
}) => {
const [dataSource, setDataSource] = useState<TableRowItem[]>([]);
const [form] = Form.useForm();
const [indicatorTypes, setIndicatorTypes] = useState<DictItem[]>([]);
const [loadingTypes, setLoadingTypes] = useState<boolean>(false);
// 获取指标类型字典
const fetchIndicatorTypes = async () => {
setLoadingTypes(true);
try {
const res = await getDictList('Indicator_type');
if (res.success && res.data) {
setIndicatorTypes(res.data);
} else {
message.error('获取指标类型失败');
}
} catch (error) {
console.error('获取指标类型失败:', error);
message.error('获取指标类型失败');
} finally {
setLoadingTypes(false);
}
};
// 将API数据转换为表格数据
const convertApiDataToTableData = (apiData: any[]): TableRowItem[] => {
// 检查数据是否已经是扁平化的表格数据格式
if (apiData.length > 0 && 'subIndicator' in apiData[0]) {
return apiData.map((item, index) => ({
...item,
key: item.id || `row-${generateUUID(16)}-${index}`,
ndId: item.id,
stScore: item.stScore || '0',
ndScore: item.score || '0',
isStar: item.isStar || StarLevel.NO,
orderBy: typeof item.stOrderBy === 'string' ? parseInt(item.stOrderBy) : item.stOrderBy,
ndOrderBy: typeof item.orderBy === 'string' ? parseInt(item.orderBy) : item.orderBy,
selected: defaultSelectedIds.includes(item.id) // 根据defaultSelectedIds设置选中状态
}));
}
// 如果是嵌套结构,需要扁平化处理
if (apiData.length > 0 && 'indicatorNdList' in apiData[0]) {
const flattenedData: TableRowItem[] = [];
apiData.forEach((stItem: any, stIndex: number) => {
stItem.indicatorNdList.forEach((ndItem: any, ndIndex: number) => {
flattenedData.push({
key: ndItem.id || `${stItem.id}-${ndIndex}`,
stId: stItem.id,
ndId: ndItem.id,
baseIndicator: stItem.baseIndicator,
descIndicator: stItem.descIndicator,
stScore: stItem.score,
indicatorType: stItem.indicatorType,
subIndicator: ndItem.subIndicator,
ndScore: ndItem.score,
isStar: ndItem.isStar || StarLevel.NO,
desc: ndItem.desc,
orderBy: typeof stItem.orderBy === 'string' ? parseInt(stItem.orderBy) : stItem.orderBy,
ndOrderBy: typeof ndItem.orderBy === 'string' ? parseInt(ndItem.orderBy) : ndItem.orderBy,
selected: defaultSelectedIds.includes(ndItem.id) // 根据defaultSelectedIds设置选中状态
});
});
});
return flattenedData;
}
return [];
};
// 将表格数据转换回API格式
const convertTableDataToApiData = (tableData: TableRowItem[]): any[] => {
// 按一级指标分组
const groupedByLevel1 = tableData.reduce((acc: Record<string, TableRowItem[]>, item: TableRowItem) => {
// 为空的baseIndicator也需要分组使用特殊键标识
const groupKey = item.baseIndicator || `empty-${item.key}`;
if (!acc[groupKey]) {
acc[groupKey] = [];
}
acc[groupKey].push(item);
return acc;
}, {});
// 转换为API需要的格式
return Object.keys(groupedByLevel1).map((groupKey, stIndex) => {
const level1Items = groupedByLevel1[groupKey];
const firstItem = level1Items[0];
// 生成唯一的临时ID
const tempStId = `temp-st-${generateUUID(16)}-${stIndex}`;
return {
id: firstItem.stId || tempStId,
baseIndicator: firstItem.baseIndicator || '',
descIndicator: firstItem.descIndicator || '',
score: firstItem.stScore || '0',
orderBy: firstItem.orderBy || stIndex + 1,
indicatorType: firstItem.indicatorType || '',
indicatorNdList: level1Items.map((item, ndIndex) => {
// 生成唯一的临时ID
const tempNdId = `temp-nd-${generateUUID(16)}-${stIndex}-${ndIndex}`;
return {
id: item.ndId || tempNdId,
subIndicator: item.subIndicator || '',
score: item.ndScore || '0',
isStar: item.isStar || StarLevel.NO,
orderBy: item.ndOrderBy || ndIndex + 1,
desc: item.desc || ''
};
})
};
});
};
// 初始化数据和获取字典
useEffect(() => {
// 获取指标类型字典
fetchIndicatorTypes();
}, []);
// 确保数据中没有重复的key
const ensureUniqueKeys = (data: TableRowItem[]): TableRowItem[] => {
const keyMap = new Map<string, boolean>();
return data.map((item, index) => {
if (!item.key || item.key.includes('undefined') || keyMap.has(item.key)) {
const newKey = `fixed-${generateUUID(32)}-${index}`;
return { ...item, key: newKey };
}
keyMap.set(item.key, true);
return item;
});
};
// 单独处理value变化
useEffect(() => {
// 初始化表格数据或value变化时更新
if (value && value.length > 0) {
// 避免不必要的状态更新,比较新旧数据是否相同
const currentValueStr = JSON.stringify(value);
const currentDataSourceApiStr = dataSource.length > 0 ?
JSON.stringify(convertTableDataToApiData(dataSource)) : '';
if (currentValueStr !== currentDataSourceApiStr) {
const tableData = convertApiDataToTableData(value);
// 保留现有项的key确保稳定性
if (dataSource && dataSource.length > 0) {
const updatedTableData = tableData.map((newItem) => {
// 尝试查找对应的现有项通过stId和ndId匹配
const existingItem = dataSource.find(existing =>
(existing.stId === newItem.stId && existing.ndId === newItem.ndId) ||
(existing.baseIndicator === newItem.baseIndicator && existing.subIndicator === newItem.subIndicator)
);
// 如果找到现有项保留其key和selected状态
if (existingItem) {
return {
...newItem,
key: existingItem.key,
selected: defaultSelectedIds.includes(newItem.ndId || '') ? true : existingItem.selected
};
}
return newItem;
});
// 确保所有key都是唯一的
setDataSource(ensureUniqueKeys(updatedTableData));
} else {
// 确保初始数据的每一项都有唯一的key
setDataSource(ensureUniqueKeys(tableData));
}
}
} else if (value && value.length === 0 && dataSource.length > 0) {
// 如果value被清空也清空dataSource
setDataSource([]);
}
}, [value]);
// 处理defaultSelectedIds变化
useEffect(() => {
if (defaultSelectedIds.length > 0 && dataSource.length > 0) {
const newData = dataSource.map(item => ({
...item,
selected: defaultSelectedIds.includes(item.ndId || '')
}));
console.log("newData",newData)
setDataSource(newData);
// 如果有onSelect回调传递所有选中的项
if (onSelect) {
const selectedItems = newData.filter(item => item.selected);
// 转换为API格式再传递给父组件
const selectedApiData = convertTableDataToApiData(selectedItems);
onSelect(selectedApiData);
}
}
}, [defaultSelectedIds]);
// 更新数据源
const updateDataSource = (newData: TableRowItem[]) => {
// 确保每行都有唯一稳定的key
const finalData = ensureUniqueKeys(newData);
setDataSource(finalData);
if (onChange) {
// 转换回API格式再传递给父组件
const apiData = convertTableDataToApiData(finalData);
onChange(apiData);
}
};
// 处理勾选状态变化
const handleCheckChange = (record: TableRowItem, checked: boolean) => {
console.log("handleCheckChange")
const newData = [...dataSource];
const index = newData.findIndex((item) => item.key === record.key);
if (index > -1) {
newData[index] = { ...newData[index], selected: checked };
setDataSource(newData);
// 如果有onSelect回调传递所有选中的项
if (onSelect) {
const selectedItems = newData.filter(item => item.selected);
// 转换为API格式再传递给父组件
const selectedApiData = convertTableDataToApiData(selectedItems);
onSelect(selectedApiData);
}
}
};
// 处理输入变化
const handleInputChange = (val: any, record: TableRowItem, field: string) => {
const newData = [...dataSource];
const index = newData.findIndex((item) => item.key === record.key);
if (index > -1) {
const item = newData[index];
// 特殊处理baseIndicator字段确保唯一性
if (field === 'baseIndicator') {
// 如果之前为空,现在有值,则视为新的一级指标
if (!item.baseIndicator && val) {
// 检查是否有重复的baseIndicator
const existingNames = newData.map((d) => d.baseIndicator).filter(Boolean);
if (existingNames.includes(val)) {
message.warning('指标名称已存在,请使用不同的名称');
return;
}
}
// 更新同组内所有行的一级指标信息
if (item.baseIndicator) {
const oldName = item.baseIndicator;
newData.forEach((row, i) => {
if (row.baseIndicator === oldName) {
newData[i] = { ...row, baseIndicator: val };
}
});
} else {
newData[index] = { ...item, [field]: val };
}
}
// 处理其他一级指标字段,需要同步到同组的所有行
else if (['descIndicator', 'stScore', 'indicatorType'].includes(field)) {
const baseIndicator = item.baseIndicator;
if (baseIndicator) {
newData.forEach((row, i) => {
if (row.baseIndicator === baseIndicator) {
newData[i] = { ...row, [field]: val };
}
});
} else {
newData[index] = { ...item, [field]: val };
}
}
// 处理二级指标字段,只更新当前行
else {
newData[index] = { ...item, [field]: val };
}
updateDataSource(newData);
}
};
// 添加一级指标
const addLevel1Indicator = (currentRecord?: TableRowItem) => {
// 使用改进后的工具函数生成唯一key
const newKey = `level1-${generateUUID(32)}`;
const newItem: TableRowItem = {
key: newKey,
baseIndicator: '', // 初始为空
descIndicator: '',
stScore: '0',
indicatorType: '',
subIndicator: '', // 注意二级指标需要为空字符串不能为undefined
ndScore: '0',
isStar: StarLevel.NO,
desc: '',
orderBy: dataSource.length + 1, // 确保正确的排序
ndOrderBy: 1, // 设置二级指标初始排序
selected: false, // 默认未选中
};
// 制作数据源的副本,避免直接修改状态
const newData = [...dataSource];
// 找到当前记录所在的一级指标组的最后一行
let insertIndex = -1;
if (currentRecord && currentRecord.baseIndicator) {
// 找到相同baseIndicator的最后一个元素
const sameGroup = newData.filter((item) => item.baseIndicator === currentRecord.baseIndicator);
const lastOfGroup = sameGroup[sameGroup.length - 1];
insertIndex = newData.findIndex((item) => item.key === lastOfGroup.key);
} else if (currentRecord && currentRecord.key) {
// 如果是新增的空白行,直接在其后插入
insertIndex = newData.findIndex((item) => item.key === currentRecord.key);
}
// 如果找到了位置,在该位置后插入,否则添加到末尾
if (insertIndex !== -1) {
newData.splice(insertIndex + 1, 0, newItem);
} else {
newData.push(newItem);
}
updateDataSource(newData);
};
// 删除一级指标
const removeLevel1Indicator = (record: TableRowItem) => {
// 如果baseIndicator为空只删除当前行
if (!record.baseIndicator) {
const newData = dataSource.filter((item) => item.key !== record.key);
updateDataSource(newData);
return;
}
// 删除所有具有相同baseIndicator的行
const newData = dataSource.filter((item) => item.baseIndicator !== record.baseIndicator);
updateDataSource(newData);
};
// 添加二级指标
const addSubIndicator = (parentKey: string) => {
// 使用工具函数生成唯一key
const newKey = `level2-${generateUUID(16)}`;
const parent = dataSource.find((item) => item.key === parentKey);
if (!parent || !parent.baseIndicator) {
message.warning('请先填写一级指标名称');
return;
}
const newItem: TableRowItem = {
key: newKey,
baseIndicator: parent.baseIndicator,
descIndicator: parent.descIndicator,
stScore: parent.stScore,
indicatorType: parent.indicatorType,
subIndicator: '',
ndScore: '0',
isStar: StarLevel.NO,
desc: '',
ndOrderBy: dataSource.filter(item => item.baseIndicator === parent.baseIndicator).length + 1,
selected: false, // 默认未选中
};
// 找到当前记录所在的位置
const newData = [...dataSource];
const parentIndex = newData.findIndex((item) => item.key === parentKey);
// 插入到父级之后
if (parentIndex !== -1) {
newData.splice(parentIndex + 1, 0, newItem);
} else {
newData.push(newItem);
}
updateDataSource(newData);
};
// 删除二级指标
const removeSubIndicator = (key: string) => {
const newData = dataSource.filter((item) => item.key !== key);
updateDataSource(newData);
};
// 获取一级指标的行数
const getLevel1RowSpan = (baseIndicator: string) => {
// 如果baseIndicator为空返回1不合并
if (!baseIndicator) {
return 1;
}
return dataSource.filter((item) => item.baseIndicator === baseIndicator).length;
};
// 处理合并单元格
const renderWithRowSpan = (
content: any,
record: TableRowItem,
render: (cellContent: any, record: TableRowItem) => React.ReactNode,
) => {
// 如果baseIndicator为空不进行合并
if (!record.baseIndicator) {
return {
children: render(content, record),
props: {
rowSpan: 1,
},
};
}
// 查找相同baseIndicator的所有项
const level1Items = dataSource.filter((item) => item.baseIndicator === record.baseIndicator);
const index = level1Items.findIndex((item) => item.key === record.key);
if (index === 0) {
return {
children: render(content, record),
props: {
rowSpan: level1Items.length,
},
};
}
return {
props: {
rowSpan: 0,
},
};
};
const columns = [
{
title: '一级指标',
children: [
{
title: '序号',
dataIndex: 'index',
align: 'center',
key: 'index',
width: 50,
render: (_: any, record: TableRowItem, index: number) => {
return <>{index + 1}</>;
},
},
{
title: '类型',
dataIndex: 'indicatorType',
align: 'center',
key: 'indicatorType',
width: 120,
render: (text: string, record: TableRowItem) => {
return renderWithRowSpan(text, record, (cellContent) => {
if (isDetail) {
// 在详情模式下,查找并显示字典中对应的名称
const typeItem = indicatorTypes.find((item) => item.code === cellContent);
return typeItem ? typeItem.dicName : cellContent || '-';
}
return (
<Select
value={cellContent}
onChange={(val) => handleInputChange(val, record, 'indicatorType')}
placeholder="请选择类型"
style={{ width: '100%' }}
loading={loadingTypes}
>
{indicatorTypes.map((item) => (
<Option key={item.code} value={item.code}>
{item.dicName}
</Option>
))}
</Select>
);
});
},
},
{
title: '基本指标',
align: 'center',
dataIndex: 'baseIndicator',
key: 'baseIndicator',
width: 150,
render: (text: string, record: TableRowItem) => {
return renderWithRowSpan(text, record, (cellContent) => {
if (isDetail) {
return cellContent || '-';
}
return (
<Input
value={cellContent}
onChange={(e) => handleInputChange(e.target.value, record, 'baseIndicator')}
placeholder="请输入基本指标"
/>
);
});
},
},
{
title: '指标说明',
align: 'center',
dataIndex: 'descIndicator',
key: 'descIndicator',
width: 200,
render: (text: string, record: TableRowItem) => {
return renderWithRowSpan(text, record, (cellContent) => {
if (isDetail) {
return cellContent || '-';
}
return (
<Input
value={cellContent}
onChange={(e) => handleInputChange(e.target.value, record, 'descIndicator')}
placeholder="请输入指标说明"
/>
);
});
},
},
{
title: '分值',
align: 'center',
dataIndex: 'stScore',
key: 'stScore',
width: 80,
render: (text: string, record: TableRowItem) => {
return renderWithRowSpan(text, record, (cellContent) => {
if (isDetail) {
return cellContent || '-';
}
return (
<InputNumber
min={0}
max={100}
value={parseFloat(cellContent) || 0}
onChange={val => handleInputChange(val?.toString() || '0', record, 'stScore')}
style={{ width: '100%' }}
/>
);
});
},
},
{
title: '操作',
key: 'level1Action',
align: 'center',
width: 100,
render: (_: any, record: TableRowItem) => {
if (isDetail) return null;
return renderWithRowSpan(null, record, () => (
<div className="action-buttons">
<Button
type="text"
icon={<PlusCircleOutlined />}
onClick={() => {
addLevel1Indicator(record);
}}
title="添加一级指标"
/>
<Popconfirm
title="确定要删除此指标吗?"
onConfirm={() => removeLevel1Indicator(record)}
okText="确定"
cancelText="取消"
>
<Button
type="text"
danger
icon={<DeleteOutlined />}
title="删除一级指标"
/>
</Popconfirm>
</div>
));
},
},
].filter((col) => !(isDetail && col.key === 'level1Action')),
},
{
title: '二级指标',
children: [
{
title: '细分指标',
dataIndex: 'subIndicator',
key: 'subIndicator',
align: 'center',
width: 200,
render: (text: string, record: TableRowItem) => {
if (isDetail) {
return text || '-';
}
return (
<Input
value={text}
onChange={(e) => handleInputChange(e.target.value, record, 'subIndicator')}
placeholder="请输入细分指标"
/>
);
},
},
{
title: '分值',
dataIndex: 'ndScore',
key: 'ndScore',
width: 80,
align: 'center',
render: (text: string, record: TableRowItem) => {
if (isDetail) {
return text || '-';
}
return (
<InputNumber
min={0}
max={100}
value={parseFloat(text) || 0}
onChange={(val) => handleInputChange(val?.toString() || '0', record, 'ndScore')}
style={{ width: '100%' }}
/>
);
},
},
{
title: '星号项',
dataIndex: 'isStar',
key: 'isStar',
align: 'center',
width: 80,
render: (text: string, record: TableRowItem) => {
if (isDetail) {
return text === StarLevel.YES ? '*' : '';
}
return (
<Select
value={text || StarLevel.NO}
onChange={val => handleInputChange(val, record, 'isStar')}
style={{ width: '100%' }}
>
<Option value={StarLevel.NO}>{StarLevelText[StarLevel.NO]}</Option>
<Option value={StarLevel.YES}>{StarLevelText[StarLevel.YES]}</Option>
</Select>
);
},
},
{
title: '评分说明',
dataIndex: 'desc',
align: 'center',
key: 'desc',
render: (text: string, record: TableRowItem) => {
if (isDetail) {
return text || '-';
}
return (
<TextArea
value={text}
onChange={(e) => handleInputChange(e.target.value, record, 'desc')}
placeholder="请输入评分说明"
autoSize={{ minRows: 1, maxRows: 3 }}
/>
);
},
},
{
title: '操作',
key: 'level2Action',
align: 'center',
width: 100,
render: (_: any, record: TableRowItem) => {
if (isDetail) return null;
return (
<div className="action-buttons">
<Button
type="text"
icon={<PlusOutlined />}
onClick={() => addSubIndicator(record.key)}
title="添加细分指标"
/>
{/* 如果该一级指标下有多个二级指标,才允许删除 */}
{getLevel1RowSpan(record.baseIndicator) > 1 && (
<Popconfirm
title="确定要删除此指标吗?"
onConfirm={() => removeSubIndicator(record.key)}
okText="确定"
cancelText="取消"
>
<Button
type="text"
danger
icon={<MinusCircleOutlined />}
title="删除细分指标"
/>
</Popconfirm>
)}
</div>
);
},
},
// 添加勾选列只在isCheck为true时显示
...(isCheck ? [{
title: '勾选',
key: 'check',
align: 'center',
width: 80,
render: (_: any, record: TableRowItem) => (
<input
type="checkbox"
checked={record.selected}
onChange={(e) => handleCheckChange(record, e.target.checked)}
/>
),
}] : []),
].filter((col) => !(isDetail && col.key === 'level2Action')),
},
];
return (
<div className="evaluate-template-table">
<Table
columns={columns}
dataSource={dataSource}
pagination={false}
bordered
rowKey="key"
size="middle"
scroll={{ x: 'max-content' }}
locale={{ emptyText: '无数据' }}
/>
{!isDetail && (
<div className="add-button-row">
<Button
type="dashed"
onClick={() => {
addLevel1Indicator();
}}
style={{ width: '100%', marginTop: 16 }}
icon={<PlusOutlined />}
>
</Button>
</div>
)}
</div>
);
};
export default EvaluateTemplateTable;