import empty from '@/Images/others/empty.png';
import { ProductType } from '@/modelType/ProductCatalog';
import type { InputRef } from 'antd';
import { Input, Switch, Table } from 'antd';
import type { FormInstance } from 'antd/es/form';
import { connect } from 'dva';
import React, {
useCallback,
useContext,
useEffect,
useRef,
useState
} from 'react';
import { DndProvider, useDrag, useDrop } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { UNVForm, UNVIcon, UNVLoading } from 'UNV-DESIGN';
import CatalogConfig from './components/CatalogConfig';
import {
dataType,
findFromData,
getParam,
ItemTypes,
optionsTyps
} from './components/DragBodyRow/DragUtils';
import Headers from './components/Headers';
import styles from './index.less';
const EditableContext = React.createContext<FormInstance<any> | null>(null);
interface DraggableBodyRowProps
extends React.HTMLAttributes<HTMLTableRowElement> {
index: number;
moveRow: (record: { [key: string]: any }) => void;
[key: string]: any;
}
//#region 拖拽行
const DraggableBodyRow = (props: DraggableBodyRowProps) => {
const {
record,
data,
index,
className,
style,
moveRow,
findRow,
...restProps
} = props;
if (!record) {
return null;
}
const [form] = UNVForm.useForm();
const {
row: originalRow,
rowIndex: originalIndex,
rowParentIndex: originalParentIndex
} = findRow(record.id);
const itemObj = {
id: record.id,
parentId: record.parentCatalogId,
index,
isGroup: record.type === dataType.group,
originalRow, // 拖拽原始数据
originalIndex, // 拖拽原始数据索引
originalParentIndex // 拖拽原始数据父节点索引
};
const isDrag = true;
const ref = useRef<HTMLTableRowElement>(null);
const [{ handlerId, isOver, dropClassName }, drop] = useDrop({
accept: ItemTypes,
collect: (monitor: any) => {
const {
id: dragId,
parentId: dragParentId,
index: dragPreIndex,
isGroup
} = monitor.getItem() || {};
if (dragId === record.id) {
return {};
}
// 是否可以拖拽替换
let isOver = monitor.isOver();
if (isGroup) {
// 要覆盖的数据是分组,或者是最外层的子项可以替换,其他情况不可以
const recordIsGroup = record.type === dataType.group;
if (!recordIsGroup) {
isOver = false;
}
} else {
// 要覆盖的数据是子项,但不在同分组不可以替换
if (dragParentId !== record.parentCatalogId) {
isOver = false;
}
}
return {
isOver,
dropClassName: 'drop-over-downward',
handlerId: monitor.getHandlerId()
};
},
hover: (item: any, monitor) => {
if (!ref.current) {
return;
}
const dragIndex = item.index;
const dropIndex = index;
// Don't replace items with themselves
if (dragIndex === dropIndex) {
return;
}
// let opt = {
// dragId: item.id, // 拖拽id
// dropId: record.id, // 要放置位置行的id
// dropParentId: record.parentCatalogId,
// operateType: optionsTyps.hover, // hover操作
// };
// moveRow(opt);
item.index = dropIndex;
},
drop: (item) => {
const opt = {
dragId: item.id, // 拖拽id
dropId: record.id, // 要放置位置行的id
dropParentId: record.parentCatalogId,
operateType: optionsTyps.drop
};
moveRow(opt);
}
});
const [{ isDragging }, drag] = useDrag({
type: ItemTypes,
item: itemObj,
collect: (monitor) => ({
isDragging: monitor.isDragging()
}),
// canDrag: (props, monitor) => isDrag //parentId不为0的才可以拖拽
end: (item, monitor) => {
const { id: droppedId, originalRow } = item;
const didDrop = monitor.didDrop();
// 超出可拖拽区域,需要将拖拽行还原
if (!didDrop) {
const opt = {
dragId: droppedId, // 拖拽id
dropId: originalRow.id, // 要放置位置行的id
dropParentId: originalRow.parentCatalogId,
originalIndex,
originalParentIndex,
operateType: optionsTyps.didDrop
};
moveRow(opt);
}
}
});
drop(drag(ref));
// 拖拽行的位置显示透明
const opacity = isDragging ? 0 : 1;
return (
<UNVForm form={form} component={false}>
<EditableContext.Provider value={form}>
<tr
ref={ref}
className={`${className}${isOver ? dropClassName : ''}`}
style={{ cursor: 'move', ...style }}
{...restProps}
/>
</EditableContext.Provider>
</UNVForm>
);
};
//#endregion 拖拽行结束
interface EditableCellProps {
title: React.ReactNode;
editable: boolean;
children: React.ReactNode;
dataIndex: keyof ProductType;
record: ProductType;
handleSave: (record: ProductType) => void;
}
//#region 暂时先这么用,提组件有bug,后续解决
const EditTableCell: React.FC<EditableCellProps> = ({
title,
editable,
children,
dataIndex,
record,
handleSave,
...restProps
}) => {
const [editing, setEditing] = useState(false);
const inputRef = useRef<InputRef>(null);
const form = useContext(EditableContext)!;
useEffect(() => {
if (editing) {
inputRef.current!.focus();
}
}, [editing]);
const toggleEdit = () => {
setEditing(!editing);
form.setFieldsValue({ [dataIndex]: record[dataIndex] });
};
const save = async () => {
try {
const values = await form.validateFields();
toggleEdit();
if (Number(values[dataIndex] !== record[dataIndex])) {
handleSave({ ...record, ...values });
}
} catch (errInfo) {
console.log('Save failed:', errInfo);
}
};
let childNode = children;
if (editable) {
childNode = editing ? (
<UNVForm.Item
style={{ margin: 0 }}
name={dataIndex}
rules={[
{
required: true,
message: intl.formatMessage({ id: 'Please enter digits' })
},
{
pattern: new RegExp(/^([0-9]){1,}$/),
message: intl.formatMessage({ id: 'Please enter digits' })
}
]}
>
<Input ref={inputRef} onPressEnter={save} onBlur={save} />
</UNVForm.Item>
) : (
<div
className="editable-cell-value-wrap"
style={{ paddingRight: 24 }}
onClick={toggleEdit}
>
{children}
</div>
);
}
return <td {...restProps}>{childNode}</td>;
};
//#endregion tablecell结束
type EditableTableProps = Parameters<typeof Table>[0];
type ColumnTypes = Exclude<EditableTableProps['columns'], undefined>;
/**
* 格式化数据
* @param list
* @returns 格式化后的数组
*/
const structuredData = (list: any) => {
const newList = list.map((item: any) => {
return {
...item,
children:
item.childList && item.childList.length ?
structuredData(item.childList) :
null
};
});
return newList;
};
/**
* 格式化,隐藏无效数据
* @param list
* @returns 格式化后的数组
*/
const structuredHideData = (list: any) => {
const newList: Array<any> = [];
const struct = (data: Array<any>, result: Array<any>) => {
data.forEach((item: any) => {
if (item.isValid) {
const newItem = { ...item, children: [] };
if (newItem.childList && newItem.childList.length > 0) {
struct(newItem.childList, newItem.children);
} else {
newItem.children = null;
}
result.push(newItem);
}
});
};
struct(list, newList);
// const newList = list.map((item: any) => {
// if (item.isValid) {
// return {
// ...item,
// children:
// item.childList && item.childList.length
// ? structuredHideData(item.childList)
// : null,
// };
// } else {
// return {};
// }
// });
return newList;
};
/**
* @description 平铺数据
* @param {any} list 待平铺数据
* @return 平铺数据
*/
const _structuredAllData = (list: any) => {
const result: Array<any> = [];
const trailData = (tempList: Array<any>) => {
if (Array.isArray(tempList) && tempList.length > 0) {
tempList.forEach((item: any) => {
result.push(item);
if (item.childList && Array.isArray(item.childList)) {
trailData(item.childList);
}
});
}
};
trailData(list);
return result;
};
const ProductCatalog = (props: { [key: string]: any }) => {
const { dispatch, queryTreeLoading, productTreeList } = props;
// table列表的数据源
const [data, setData] = useState<ProductType[]>([]);
// 展示筛选配置弹窗
const [screenModalData, setScreenModalData] = useState<ProductType | {}>({});
// table默认展开行
const [expandKeys, setExpandKeys] = useState<React.Key[]>([]);
// 是否隐藏无效项
const [hideValid, setHideValid] = useState(false);
useEffect(() => {
dispatch({
type: 'productCatalog/queryProductCatalogTree',
payload: {}
});
}, []);
useEffect(() => {
if (Array.isArray(productTreeList)) {
if (!hideValid) {
setData(structuredData(productTreeList));
} else {
setData(structuredHideData(productTreeList));
}
}
}, [productTreeList, hideValid]);
const updateCatalog = (obj: {
id: number;
isValid?: boolean;
sort_no?: number;
}) => {
dispatch({
type: 'productCatalog/productCatalog',
payload: { ...obj, productCatalogCode: 'EXT' }
});
};
const handleSave = (row: ProductType) => {
// const newData = [...data];
// const index = newData.findIndex((item) => row.id === item.id);
// const item = newData[index];
// newData.splice(index, 1, {
// ...item,
// ...row,
// });
// setData(newData);
updateCatalog({ id: row.id, sort_no: row.sortNo });
};
const findRow = (id: number | string) => {
const { row, index, parentIndex } = findFromData(data, id);
return {
row,
rowIndex: index,
rowParentIndex: parentIndex
};
};
//#region 移动目录后逻辑处理
/**
* 移动目录后逻辑处理
*/
const moveRow = useCallback(
(props: any) => {
const { dragId, dropId, dropParentId, operateType, originalIndex } =
props;
const {
dragRow,
dropRow,
dragIndex = 0,
dropIndex = 0,
dragParentIndex, // 拖拽子节点的父节点索引
dropParentIndex, // 放置子节点父节点索引
dragTopLevelIndex, // 拖拽的顶级节点索引
dropTopLevelIndex // 放置的顶级节点索引
} = getParam(data, dragId, dropId);
// 拖拽是否是组
const dragIsGroup =
dragRow.type === dataType.group || !dragRow.parentCatalogId;
// 放置的是否是组
const dropIsGroup = !dropParentId;
// 根据变化的数据查找拖拽行的row和索引
const {
row,
index: rowIndex = 0,
parentIndex: rowParentIndex
} = findFromData(data, dragId);
const newData = data;
// 组拖拽
if (dragIsGroup && dropIsGroup) {
// 超出出拖拽区域还原
if (operateType === optionsTyps.didDrop) {
// 【暂留】超出拖拽区域
// newData = update(data, {
// $splice: [
// [rowIndex, 1], //删除目前拖拽的索引的数据
// [originalIndex, 0, row], // 将拖拽数据插入原始索引位置
// ],
// });
} else if (dragIndex !== dropIndex) {
// newData = update(data, {
// $splice: [
// [dragIndex, 1],
// [dropIndex, 0, dragRow],
// ],
// });
dispatch({
type: 'productCatalog/updateProductCatalogSortNoByDrag',
payload: {
dragType: dragIndex < dropIndex ? 0 : 1,
productCatalogId: dragRow.id,
targetProductCatalogId: dropRow.id
}
});
}
} else if (dragRow.parentCatalogId === dropRow?.parentCatalogId) {
// 同一组下的子项拖拽
if (
dragTopLevelIndex === dropTopLevelIndex &&
typeof dragTopLevelIndex === 'number' &&
typeof dropTopLevelIndex === 'number'
) {
// 超出拖拽区域还原
if (operateType === optionsTyps.didDrop) {
// 【暂留】超出拖拽区域
// newData = update(data, {
// [dragTopLevelIndex]: {
// children: {
// [dragParentIndex]: {
// children: {
// $splice: [
// [rowIndex, 1],
// [originalIndex, 0, row],
// ],
// },
// },
// },
// },
// });
} else if (dragIndex !== dropIndex) {
// newData = update(data, {
// [dragTopLevelIndex]: {
// children: {
// [dragParentIndex]: {
// children: {
// $splice: [
// [dragIndex, 1],
// [dropIndex, 0, dragRow],
// ],
// },
// },
// },
// },
// });
dispatch({
type: 'productCatalog/updateProductCatalogSortNoByDrag',
payload: {
dragType: dragIndex < dropIndex ? 0 : 1,
productCatalogId: dragRow.id,
targetProductCatalogId: dropRow.id
}
});
}
} else if (!dropTopLevelIndex && !dragTopLevelIndex) {
// 超出拖拽区域还原
if (operateType === optionsTyps.didDrop) {
// 【暂留】超出拖拽区域
// newData = update(data, {
// [dragParentIndex]: {
// children: {
// $splice: [
// [rowIndex, 1],
// [originalIndex, 0, row],
// ],
// },
// },
// });
} else if (dragIndex !== dropIndex) {
// newData = update(data, {
// [dragParentIndex]: {
// children: {
// $splice: [
// [dragIndex, 1],
// [dropIndex, 0, dragRow],
// ],
// },
// },
// });
dispatch({
type: 'productCatalog/updateProductCatalogSortNoByDrag',
payload: {
dragType: dragIndex < dropIndex ? 0 : 1,
productCatalogId: dragRow.id,
targetProductCatalogId: dropRow.id
}
});
}
}
}
},
[data]
);
//#endregion
const showScreen = (record: ProductType) => {
setScreenModalData(record);
};
const defaultColumns: (ColumnTypes[number] & {
editable?: boolean;
dataIndex: string;
})[] = [
{
title: intl.formatMessage({ id: 'Name1' }),
align: 'left',
dataIndex: 'productCatalogName',
width: 380,
ellipsis: true
},
{
title: intl.formatMessage({ id: 'Level1' }),
align: 'center',
dataIndex: 'catalogLevel',
render: (text: number) => {
switch (text) {
case 1:
return intl.formatMessage({ id: 'Level I' });
case 2:
return intl.formatMessage({ id: 'Level II' });
case 3:
return intl.formatMessage({ id: 'Level III' });
default:
return intl.formatMessage({ id: 'N/A' });
}
}
},
{
title: intl.formatMessage({ id: 'Order' }),
align: 'center',
dataIndex: 'sortNo',
editable: true
},
// {
// title: '备注',
// align: 'center',
// dataIndex: '',
// width: 340,
// ellipsis: true,
// },
{
title: intl.formatMessage({ id: 'Display on UI' }),
align: 'center',
dataIndex: 'isValid',
render: (isValid: boolean, record: ProductType) => (
<Switch
checked={isValid}
onChange={() => {
updateCatalog({ id: record.id, isValid: !isValid });
}}
/>
)
},
{
title: intl.formatMessage({ id: 'Advanced Filter Config' }),
align: 'center',
dataIndex: 'operation',
render: (_: any, record: ProductType) => (
<UNVIcon
icon="configIcon"
onClick={() => {
showScreen(record);
}}
/>
)
}
];
const columns = defaultColumns.map((col) => {
if (!col.editable) {
return col;
}
return {
...col,
onCell: (record: ProductType) => ({
record,
editable: col.editable,
dataIndex: col.dataIndex,
title: col.title,
handleSave
})
};
});
return (
<div className={styles.productCatalog_index_container}>
<UNVLoading loading={queryTreeLoading}>
<div className={styles.productCatalog_index_title}>
{intl.formatMessage({ id: 'DIMS Product Catalog' })}
</div>
<Headers
onFirstAction={() => {
setExpandKeys(_structuredAllData(data).map((item: any) => item.id));
}}
onSecondAction={() => {
setExpandKeys([]);
}}
onThirdAction={() => {
dispatch({
type: 'productCatalog/resetProductCatalogSortNo',
payload: {}
});
}}
check={hideValid}
setCheck={setHideValid}
/>
<div className={styles.productCatalog_index_content}>
<DndProvider backend={HTML5Backend}>
<Table
size="small"
rowKey="id"
columns={columns as ColumnTypes}
dataSource={data}
bordered
pagination={false}
scroll={{ y: 500 }}
components={{
body: {
row: DraggableBodyRow,
cell: EditTableCell
}
}}
onRow={(record: any, index: number) => ({
record,
data,
index,
moveRow,
findRow
})}
expandable={{
expandedRowKeys: expandKeys,
onExpand: (_: boolean, row: ProductType) => {
setExpandKeys(() => {
if (expandKeys.includes(row.id)) {
return expandKeys.filter(
(item: React.Key) => item !== row.id
);
}
return [...expandKeys, row.id];
});
}
}}
locale={{
emptyText: (
<div
className="empty"
style={{ height: '100%', paddingTop: '10%' }}
>
<img src={empty} />
<div>{intl.formatMessage({ id: 'No data' })}</div>
</div>
)
}}
/>
</DndProvider>
</div>
</UNVLoading>
</div>
);
};
export default connect(
({ productCatalog, loading }: { [key: string]: any }) => {
const { productTreeList } = productCatalog;
return {
productTreeList,
queryTreeLoading:
loading.effects['productCatalog/queryProductCatalogTree'] || false
};
}
)(ProductCatalog);
table样式设置
:global {
.ant4-modal-footer {
margin-top: -20px;
.ant-spin-nested-loading,
.ant-spin-container,
.ant-table,
.ant-table-container,
.ant-table-content {
width: 100%;
height: 100%;
}
.ant-table-container {
background-color: #f7f8fa;
}
.ant-table-row {
background-color: white;
}
}
}