开发对接供应商评价管理

This commit is contained in:
linxd
2025-06-23 19:15:13 +08:00
parent 402d3dd575
commit b9bbc906bf
35 changed files with 5288 additions and 621 deletions

View File

@ -0,0 +1,709 @@
// 供应商评价 模板管理新增中的table
import React, { useState, useEffect } from 'react';
import {
Table,
Input,
Button,
Select,
Form,
InputNumber,
Upload,
message,
Divider,
Switch,
} from 'antd';
import {
PlusOutlined,
MinusCircleOutlined,
UploadOutlined,
PlusCircleOutlined,
DeleteOutlined,
} from '@ant-design/icons';
import { getDictList, DictItem } from '@/servers/api/dicts';
import { StarLevel, StarLevelText } from '@/dicts/supplierTemplateDict';
import './EvaluateTemplateTable.less';
// 注意这里我们使用any类型是为了灵活处理多种数据格式
// 实际上API的类型定义在 src/servers/api/typings.d.ts 中的 SupplierEvaluate 命名空间下
// 包含了 IndicatorStItem 和 IndicatorNdItem 类型
const { Option } = Select;
const { TextArea } = Input;
interface EvaluateTemplateTableProps {
value?: any[];
onChange?: (value: any[]) => void;
isDetail?: boolean; // 是否详情展示用如果为true则将input都改为text展示并且将操作列隐藏
}
// 内部使用的数据结构,扁平化后的行数据
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;
}
const EvaluateTemplateTable: React.FC<EvaluateTemplateTableProps> = ({
value = [],
onChange,
isDetail = false,
}) => {
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) => ({
key: item.id || `row-${index}`,
stId: item.stId,
ndId: item.id,
baseIndicator: item.baseIndicator || '',
descIndicator: item.descIndicator || '',
stScore: item.stScore || '0',
indicatorType: item.indicatorType || '',
subIndicator: item.subIndicator || '',
ndScore: item.score || '0',
isStar: item.isStar || StarLevel.NO,
desc: item.desc || '',
orderBy: typeof item.stOrderBy === 'string' ? parseInt(item.stOrderBy) : item.stOrderBy,
ndOrderBy: typeof item.orderBy === 'string' ? parseInt(item.orderBy) : item.orderBy
}));
}
// 如果是嵌套结构,需要扁平化处理
if (apiData.length > 0 && 'indicatorNdList' in apiData[0]) {
const flattenedData: TableRowItem[] = [];
apiData.forEach((stItem: any) => {
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,
desc: ndItem.desc,
orderBy: typeof stItem.orderBy === 'string' ? parseInt(stItem.orderBy) : stItem.orderBy,
ndOrderBy: typeof ndItem.orderBy === 'string' ? parseInt(ndItem.orderBy) : ndItem.orderBy
});
});
});
return flattenedData;
}
// 如果是旧格式,尝试兼容处理
if (apiData.length > 0 && 'level1Name' in apiData[0]) {
return apiData.map((item, index) => ({
key: item.key || `legacy-${index}`,
stId: item._originalData?.stItem?.id,
ndId: item._originalData?.ndItem?.id,
baseIndicator: item.level1Name || '',
descIndicator: item.level1Description || '',
stScore: item.level1Score?.toString() || '0',
indicatorType: item.indicator_type || '',
subIndicator: item.level2Name || '',
ndScore: item.level2Score?.toString() || '0',
isStar: item.isStar || StarLevel.NO,
desc: item.desc_score || '',
orderBy: typeof item.orderBy === 'string' ? parseInt(item.orderBy) : (item.orderBy || 0),
ndOrderBy: typeof item.ndOrderBy === 'string' ? parseInt(item.ndOrderBy) : (item.ndOrderBy || 0)
}));
}
return [];
};
// 将表格数据转换回API格式
const convertTableDataToApiData = (tableData: TableRowItem[]): any[] => {
// 按一级指标分组
const groupedByLevel1 = tableData.reduce((acc: Record<string, TableRowItem[]>, item: TableRowItem) => {
const baseIndicator = item.baseIndicator;
if (!baseIndicator) return acc;
if (!acc[baseIndicator]) {
acc[baseIndicator] = [];
}
acc[baseIndicator].push(item);
return acc;
}, {});
// 转换为API需要的格式
return Object.keys(groupedByLevel1).map((baseIndicator, stIndex) => {
const level1Items = groupedByLevel1[baseIndicator];
const firstItem = level1Items[0];
// 使用any类型避免类型错误
const stItem: any = {
id: firstItem.stId,
baseIndicator,
descIndicator: firstItem.descIndicator,
score: firstItem.stScore,
orderBy: firstItem.orderBy || stIndex + 1,
indicatorType: firstItem.indicatorType,
indicatorNdList: level1Items.map((item, ndIndex) => ({
id: item.ndId,
subIndicator: item.subIndicator,
score: item.ndScore,
isStar: item.isStar,
orderBy: item.ndOrderBy || ndIndex + 1,
desc: item.desc
}))
};
return stItem;
});
};
// 初始化数据和获取字典
useEffect(() => {
// 获取指标类型字典
fetchIndicatorTypes();
// 初始化表格数据
if (value && value.length > 0) {
const tableData = convertApiDataToTableData(value);
setDataSource(tableData);
}
}, [value]);
// 更新数据源
const updateDataSource = (newData: TableRowItem[]) => {
setDataSource(newData);
if (onChange) {
// 转换回API格式再传递给父组件
const apiData = convertTableDataToApiData(newData);
onChange(apiData);
}
};
// 处理输入变化
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) => {
const newKey = `level1-${Date.now()}`;
const newItem: TableRowItem = {
key: newKey,
baseIndicator: '', // 初始为空
descIndicator: '',
stScore: '0',
indicatorType: '',
subIndicator: '',
ndScore: '0',
isStar: StarLevel.NO,
desc: '',
};
// 找到当前记录所在的位置
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) => {
const newKey = `level2-${Date.now()}`;
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: '',
};
// 找到当前记录所在的位置
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 renderWithRowSpan(index + 1, record, (cellContent) => cellContent);
},
},
{
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="添加一级指标"
/>
<Button
type="text"
danger
icon={<DeleteOutlined />}
onClick={() => removeLevel1Indicator(record)}
title="删除一级指标"
/>
</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 && (
<Button
type="text"
danger
icon={<MinusCircleOutlined />}
onClick={() => removeSubIndicator(record.key)}
title="删除细分指标"
/>
)}
</div>
);
},
},
].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 && dataSource.length > 0 && (
<div className="add-button-row">
<Button
type="dashed"
onClick={() => addLevel1Indicator()}
style={{ width: '100%', marginTop: 16 }}
icon={<PlusOutlined />}
>
</Button>
</div>
)}
{!isDetail && dataSource.length === 0 && (
<div className="add-button-row">
<Button
type="dashed"
onClick={() => addLevel1Indicator()}
style={{ width: '100%', marginTop: 16 }}
icon={<PlusOutlined />}
>
</Button>
</div>
)}
</div>
);
};
export default EvaluateTemplateTable;