2025-06-23 19:15:13 +08:00
|
|
|
|
// 供应商评价 模板管理新增中的table
|
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
|
|
|
import {
|
|
|
|
|
Table,
|
|
|
|
|
Input,
|
|
|
|
|
Button,
|
|
|
|
|
Select,
|
|
|
|
|
Form,
|
|
|
|
|
InputNumber,
|
|
|
|
|
message,
|
2025-06-27 17:15:45 +08:00
|
|
|
|
Popconfirm,
|
2025-06-23 19:15:13 +08:00
|
|
|
|
} from 'antd';
|
|
|
|
|
import {
|
|
|
|
|
PlusOutlined,
|
|
|
|
|
MinusCircleOutlined,
|
|
|
|
|
PlusCircleOutlined,
|
|
|
|
|
DeleteOutlined,
|
|
|
|
|
} from '@ant-design/icons';
|
|
|
|
|
import { getDictList, DictItem } from '@/servers/api/dicts';
|
|
|
|
|
import { StarLevel, StarLevelText } from '@/dicts/supplierTemplateDict';
|
2025-06-23 20:29:01 +08:00
|
|
|
|
import { generateUUID } from '@/utils/utils';
|
2025-06-23 19:15:13 +08:00
|
|
|
|
import './EvaluateTemplateTable.less';
|
|
|
|
|
|
|
|
|
|
const { Option } = Select;
|
|
|
|
|
const { TextArea } = Input;
|
|
|
|
|
|
|
|
|
|
interface EvaluateTemplateTableProps {
|
|
|
|
|
value?: any[];
|
|
|
|
|
onChange?: (value: any[]) => void;
|
|
|
|
|
isDetail?: boolean; // 是否详情展示用,如果为true则将input都改为text展示并且将操作列隐藏
|
2025-06-24 20:32:55 +08:00
|
|
|
|
isCheck?: boolean; // 是否显示勾选操作,如果为true则在表格最后一列增加勾选操作项
|
|
|
|
|
onSelect?: (selectedItems: any[]) => void; // 勾选回调函数
|
|
|
|
|
defaultSelectedIds?: string[]; // 默认选中的二级指标ID数组
|
2025-06-23 19:15:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 内部使用的数据结构,扁平化后的行数据
|
|
|
|
|
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;
|
2025-06-24 20:32:55 +08:00
|
|
|
|
selected?: boolean; // 是否选中
|
2025-06-23 19:15:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const EvaluateTemplateTable: React.FC<EvaluateTemplateTableProps> = ({
|
|
|
|
|
value = [],
|
|
|
|
|
onChange,
|
|
|
|
|
isDetail = false,
|
2025-06-24 20:32:55 +08:00
|
|
|
|
isCheck = false,
|
|
|
|
|
onSelect,
|
|
|
|
|
defaultSelectedIds = [],
|
2025-06-23 19:15:13 +08:00
|
|
|
|
}) => {
|
|
|
|
|
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) => ({
|
2025-06-23 20:29:01 +08:00
|
|
|
|
...item,
|
|
|
|
|
key: item.id || `row-${generateUUID(16)}-${index}`,
|
2025-06-23 19:15:13 +08:00
|
|
|
|
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,
|
2025-06-24 20:32:55 +08:00
|
|
|
|
ndOrderBy: typeof item.orderBy === 'string' ? parseInt(item.orderBy) : item.orderBy,
|
|
|
|
|
selected: defaultSelectedIds.includes(item.id) // 根据defaultSelectedIds设置选中状态
|
2025-06-23 19:15:13 +08:00
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 如果是嵌套结构,需要扁平化处理
|
|
|
|
|
if (apiData.length > 0 && 'indicatorNdList' in apiData[0]) {
|
|
|
|
|
const flattenedData: TableRowItem[] = [];
|
|
|
|
|
|
2025-06-23 20:29:01 +08:00
|
|
|
|
apiData.forEach((stItem: any, stIndex: number) => {
|
2025-06-23 19:15:13 +08:00
|
|
|
|
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,
|
2025-06-23 20:29:01 +08:00
|
|
|
|
isStar: ndItem.isStar || StarLevel.NO,
|
2025-06-23 19:15:13 +08:00
|
|
|
|
desc: ndItem.desc,
|
|
|
|
|
orderBy: typeof stItem.orderBy === 'string' ? parseInt(stItem.orderBy) : stItem.orderBy,
|
2025-06-24 20:32:55 +08:00
|
|
|
|
ndOrderBy: typeof ndItem.orderBy === 'string' ? parseInt(ndItem.orderBy) : ndItem.orderBy,
|
|
|
|
|
selected: defaultSelectedIds.includes(ndItem.id) // 根据defaultSelectedIds设置选中状态
|
2025-06-23 19:15:13 +08:00
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return flattenedData;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return [];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 将表格数据转换回API格式
|
|
|
|
|
const convertTableDataToApiData = (tableData: TableRowItem[]): any[] => {
|
|
|
|
|
// 按一级指标分组
|
|
|
|
|
const groupedByLevel1 = tableData.reduce((acc: Record<string, TableRowItem[]>, item: TableRowItem) => {
|
2025-06-23 21:39:51 +08:00
|
|
|
|
// 为空的baseIndicator也需要分组,使用特殊键标识
|
2025-06-23 20:29:01 +08:00
|
|
|
|
const groupKey = item.baseIndicator || `empty-${item.key}`;
|
2025-06-23 19:15:13 +08:00
|
|
|
|
|
2025-06-23 20:29:01 +08:00
|
|
|
|
if (!acc[groupKey]) {
|
|
|
|
|
acc[groupKey] = [];
|
2025-06-23 19:15:13 +08:00
|
|
|
|
}
|
2025-06-23 20:29:01 +08:00
|
|
|
|
acc[groupKey].push(item);
|
2025-06-23 19:15:13 +08:00
|
|
|
|
return acc;
|
|
|
|
|
}, {});
|
|
|
|
|
|
|
|
|
|
// 转换为API需要的格式
|
2025-06-23 20:29:01 +08:00
|
|
|
|
return Object.keys(groupedByLevel1).map((groupKey, stIndex) => {
|
|
|
|
|
const level1Items = groupedByLevel1[groupKey];
|
2025-06-23 19:15:13 +08:00
|
|
|
|
const firstItem = level1Items[0];
|
|
|
|
|
|
2025-06-23 20:29:01 +08:00
|
|
|
|
// 生成唯一的临时ID
|
|
|
|
|
const tempStId = `temp-st-${generateUUID(16)}-${stIndex}`;
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
id: firstItem.stId || tempStId,
|
|
|
|
|
baseIndicator: firstItem.baseIndicator || '',
|
|
|
|
|
descIndicator: firstItem.descIndicator || '',
|
|
|
|
|
score: firstItem.stScore || '0',
|
2025-06-23 19:15:13 +08:00
|
|
|
|
orderBy: firstItem.orderBy || stIndex + 1,
|
2025-06-23 20:29:01 +08:00
|
|
|
|
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 || ''
|
|
|
|
|
};
|
|
|
|
|
})
|
2025-06-23 19:15:13 +08:00
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 初始化数据和获取字典
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
// 获取指标类型字典
|
|
|
|
|
fetchIndicatorTypes();
|
2025-06-23 20:29:01 +08:00
|
|
|
|
}, []);
|
2025-06-23 19:15:13 +08:00
|
|
|
|
|
2025-06-23 21:39:51 +08:00
|
|
|
|
// 确保数据中没有重复的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;
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2025-06-23 20:29:01 +08:00
|
|
|
|
// 单独处理value变化
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
// 初始化表格数据或value变化时更新
|
2025-06-23 19:15:13 +08:00
|
|
|
|
if (value && value.length > 0) {
|
2025-06-23 20:29:01 +08:00
|
|
|
|
// 避免不必要的状态更新,比较新旧数据是否相同
|
|
|
|
|
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) {
|
2025-06-23 21:39:51 +08:00
|
|
|
|
const updatedTableData = tableData.map((newItem) => {
|
2025-06-23 20:29:01 +08:00
|
|
|
|
// 尝试查找对应的现有项,通过stId和ndId匹配
|
|
|
|
|
const existingItem = dataSource.find(existing =>
|
|
|
|
|
(existing.stId === newItem.stId && existing.ndId === newItem.ndId) ||
|
|
|
|
|
(existing.baseIndicator === newItem.baseIndicator && existing.subIndicator === newItem.subIndicator)
|
|
|
|
|
);
|
|
|
|
|
|
2025-06-24 20:32:55 +08:00
|
|
|
|
// 如果找到现有项,保留其key和selected状态
|
2025-06-23 20:29:01 +08:00
|
|
|
|
if (existingItem) {
|
2025-06-24 20:32:55 +08:00
|
|
|
|
return {
|
|
|
|
|
...newItem,
|
|
|
|
|
key: existingItem.key,
|
|
|
|
|
selected: defaultSelectedIds.includes(newItem.ndId || '') ? true : existingItem.selected
|
|
|
|
|
};
|
2025-06-23 20:29:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-06-23 21:39:51 +08:00
|
|
|
|
return newItem;
|
2025-06-23 20:29:01 +08:00
|
|
|
|
});
|
|
|
|
|
|
2025-06-23 21:39:51 +08:00
|
|
|
|
// 确保所有key都是唯一的
|
|
|
|
|
setDataSource(ensureUniqueKeys(updatedTableData));
|
2025-06-23 20:29:01 +08:00
|
|
|
|
} else {
|
|
|
|
|
// 确保初始数据的每一项都有唯一的key
|
2025-06-23 21:39:51 +08:00
|
|
|
|
setDataSource(ensureUniqueKeys(tableData));
|
2025-06-23 20:29:01 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else if (value && value.length === 0 && dataSource.length > 0) {
|
|
|
|
|
// 如果value被清空,也清空dataSource
|
|
|
|
|
setDataSource([]);
|
2025-06-23 19:15:13 +08:00
|
|
|
|
}
|
|
|
|
|
}, [value]);
|
|
|
|
|
|
2025-06-24 20:32:55 +08:00
|
|
|
|
// 处理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]);
|
|
|
|
|
|
2025-06-23 19:15:13 +08:00
|
|
|
|
// 更新数据源
|
|
|
|
|
const updateDataSource = (newData: TableRowItem[]) => {
|
2025-06-23 20:29:01 +08:00
|
|
|
|
// 确保每行都有唯一稳定的key
|
2025-06-23 21:39:51 +08:00
|
|
|
|
const finalData = ensureUniqueKeys(newData);
|
2025-06-23 20:29:01 +08:00
|
|
|
|
|
|
|
|
|
setDataSource(finalData);
|
2025-06-23 19:15:13 +08:00
|
|
|
|
if (onChange) {
|
|
|
|
|
// 转换回API格式再传递给父组件
|
2025-06-23 20:29:01 +08:00
|
|
|
|
const apiData = convertTableDataToApiData(finalData);
|
2025-06-23 19:15:13 +08:00
|
|
|
|
onChange(apiData);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-06-24 20:32:55 +08:00
|
|
|
|
// 处理勾选状态变化
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-06-23 19:15:13 +08:00
|
|
|
|
// 处理输入变化
|
|
|
|
|
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) => {
|
2025-06-23 21:39:51 +08:00
|
|
|
|
// 使用改进后的工具函数生成唯一key
|
2025-06-23 20:29:01 +08:00
|
|
|
|
const newKey = `level1-${generateUUID(32)}`;
|
2025-06-23 19:15:13 +08:00
|
|
|
|
|
|
|
|
|
const newItem: TableRowItem = {
|
|
|
|
|
key: newKey,
|
|
|
|
|
baseIndicator: '', // 初始为空
|
|
|
|
|
descIndicator: '',
|
|
|
|
|
stScore: '0',
|
|
|
|
|
indicatorType: '',
|
2025-06-23 20:29:01 +08:00
|
|
|
|
subIndicator: '', // 注意:二级指标需要为空字符串,不能为undefined
|
2025-06-23 19:15:13 +08:00
|
|
|
|
ndScore: '0',
|
|
|
|
|
isStar: StarLevel.NO,
|
|
|
|
|
desc: '',
|
2025-06-23 20:29:01 +08:00
|
|
|
|
orderBy: dataSource.length + 1, // 确保正确的排序
|
2025-06-24 20:32:55 +08:00
|
|
|
|
ndOrderBy: 1, // 设置二级指标初始排序
|
|
|
|
|
selected: false, // 默认未选中
|
2025-06-23 19:15:13 +08:00
|
|
|
|
};
|
|
|
|
|
|
2025-06-23 20:29:01 +08:00
|
|
|
|
// 制作数据源的副本,避免直接修改状态
|
2025-06-23 19:15:13 +08:00
|
|
|
|
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) => {
|
2025-06-23 20:29:01 +08:00
|
|
|
|
// 使用工具函数生成唯一key
|
|
|
|
|
const newKey = `level2-${generateUUID(16)}`;
|
|
|
|
|
|
2025-06-23 19:15:13 +08:00
|
|
|
|
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: '',
|
2025-06-24 20:32:55 +08:00
|
|
|
|
ndOrderBy: dataSource.filter(item => item.baseIndicator === parent.baseIndicator).length + 1,
|
|
|
|
|
selected: false, // 默认未选中
|
2025-06-23 19:15:13 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 找到当前记录所在的位置
|
|
|
|
|
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) => {
|
2025-06-24 14:00:51 +08:00
|
|
|
|
return <>{index + 1}</>;
|
2025-06-23 19:15:13 +08:00
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
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 />}
|
2025-06-23 20:29:01 +08:00
|
|
|
|
onClick={() => {
|
|
|
|
|
addLevel1Indicator(record);
|
|
|
|
|
}}
|
2025-06-23 19:15:13 +08:00
|
|
|
|
title="添加一级指标"
|
|
|
|
|
/>
|
2025-06-27 17:15:45 +08:00
|
|
|
|
<Popconfirm
|
|
|
|
|
title="确定要删除此指标吗?"
|
|
|
|
|
onConfirm={() => removeLevel1Indicator(record)}
|
|
|
|
|
okText="确定"
|
|
|
|
|
cancelText="取消"
|
|
|
|
|
>
|
2025-06-27 17:48:37 +08:00
|
|
|
|
<Button
|
|
|
|
|
type="text"
|
|
|
|
|
danger
|
|
|
|
|
icon={<DeleteOutlined />}
|
|
|
|
|
title="删除一级指标"
|
|
|
|
|
/>
|
2025-06-27 17:15:45 +08:00
|
|
|
|
</Popconfirm>
|
2025-06-23 19:15:13 +08:00
|
|
|
|
</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 && (
|
2025-06-27 17:15:45 +08:00
|
|
|
|
<Popconfirm
|
|
|
|
|
title="确定要删除此指标吗?"
|
|
|
|
|
onConfirm={() => removeSubIndicator(record.key)}
|
|
|
|
|
okText="确定"
|
|
|
|
|
cancelText="取消"
|
|
|
|
|
>
|
2025-06-27 17:48:37 +08:00
|
|
|
|
<Button
|
|
|
|
|
type="text"
|
|
|
|
|
danger
|
|
|
|
|
icon={<MinusCircleOutlined />}
|
|
|
|
|
title="删除细分指标"
|
|
|
|
|
/>
|
2025-06-27 17:15:45 +08:00
|
|
|
|
</Popconfirm>
|
2025-06-23 19:15:13 +08:00
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
},
|
2025-06-24 20:32:55 +08:00
|
|
|
|
// 添加勾选列,只在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)}
|
|
|
|
|
/>
|
|
|
|
|
),
|
|
|
|
|
}] : []),
|
2025-06-23 19:15:13 +08:00
|
|
|
|
].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: '无数据' }}
|
|
|
|
|
/>
|
2025-06-23 20:29:01 +08:00
|
|
|
|
{!isDetail && (
|
2025-06-23 19:15:13 +08:00
|
|
|
|
<div className="add-button-row">
|
|
|
|
|
<Button
|
|
|
|
|
type="dashed"
|
2025-06-23 20:29:01 +08:00
|
|
|
|
onClick={() => {
|
|
|
|
|
addLevel1Indicator();
|
|
|
|
|
}}
|
2025-06-23 19:15:13 +08:00
|
|
|
|
style={{ width: '100%', marginTop: 16 }}
|
|
|
|
|
icon={<PlusOutlined />}
|
|
|
|
|
>
|
|
|
|
|
添加一级指标
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default EvaluateTemplateTable;
|