Commit 0c89916f authored by 李纪文's avatar 李纪文

fix: 修改设备树业务组件

parent de5c1917
......@@ -27,13 +27,34 @@ group:
api 参考 Antd Tree 组件 https://ant.design/components/tree-cn/
| 参数 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| prefix | 搜索框的前置图标 | ReactNode | 搜索 icon |
| placeholder | 搜索框占位符 | string | 搜索设备名称 |
| checkable | 节点前添加 Checkbox 复选框 | boolean | false |
| pagination | 默认分页,false 取消分页 | boolean | true |
| serviceParams | 服务参数 | object | { pageIndex: 1, pageSize: 500, deviceTypes: '二供泵房,二供机组', getChild: true, userID: 1, queryInfo: '', sortFields: '', direction: '', isTop: true } |
| deviceTreeService `必需` | 设备树服务 | promise | - |
| onTreeCheck | 点击复选框触发 | function(checkedNodes){ } | - |
| onTreeSelect | 点击树节点触发 | function(selectedNodes){ } | - |
| 参数 | 说明 | 类型 | 默认值 |
| ------------- | -------------------------------- | --------------------- | -------- |
| deviceTypes | 设备类型, 多种设备逗号隔开 | string | 二供泵房 |
| userAccessor | 是否启用设备权限过滤 | boolean | false |
| getChild | 是否查询子设备 | boolean | false |
| sortFields | 设备排序字段, 设备台账表中的字段 | string | - |
| direction | 设备排序方向, asc/desc | string | - |
| classField | 分组字段,设备台账表中的字段 | string | - |
| customerName | 服务参数 | string | - |
| onSelect | 选择回调 | function(data: []){ } | - |
| onCheck | 多选回调 | function(data: []){ } | - |
| setDeviceList | 返回设备回调 | function(data: []){ } | - |
| setSearchStr | 返回搜索字符串 | function(val: ''){ } | - |
| selectable | 'Antd-Tree 属性' | function(data: []){ } | - |
| checkable | 'Antd-Tree 属性' | function(data: []){ } | - |
| keepChecked | 是否选择第一个 | boolean | false |
## 补充说明
- keepChecked: boolean, 搜索之后保持之前的选中数据
- 第一次加载数据后默认勾选第一条
- 再次加载数据后保持之前的选中数据,不默认勾选第一条
- 选中数据之后和之前的做合并处理,key 也做合并处理做保留
- 潜在问题 1: 取消选中后,区分取消选中的数据 和 之前保留但不再树中的数据(利用 onCheck 会保留多余的 key)
- singleType: boolean, 是否只能选单一类型设备, 其他类型设备会被禁用
- 单选及多选说明:
- 仅多选: 传递 checkable=true, selectable=false, 触发 onCheck
- 仅单选: 传递 checkable=false, selectable=true, 触发 onSelect
- 混合: 传递 checkable=true, selectable=true, 触发 onCheck(混合模式表现:点击前面多选框可多选设备, 点击设备名称可直接切换单选到该设备)
import { request } from '@wisdom-utils/utils/es';
const REQUEST_METHOD_GET = 'get';
const REQUEST_METHOD_POST = 'post';
// eslint-disable-next-line no-undef
const baseURI = typeof DUMI_TYPE !== 'undefined' && DUMI_TYPE === 'dumi' ? '/api' : '';
export function getEquipmentInfo(data) {
return request({
url: `${baseURI}/PandaMonitor/Monitor/Device/GetEquipmentInfo`,
method: REQUEST_METHOD_POST,
data,
});
}
import React from 'react';
import PandaDeviceTree from '../index';
import { service } from '@wisdom-utils/utils';
const REQUEST_HTTP = 'http';
const REQUEST_METHOD_POST = 'post';
const GET_DEVICE_LIST =
'https://www.fastmock.site/mock/162c15dca15c4dba9ba51e0a0b76929b/api/Publish/GCK/Device/DeviceTree'; //获取设备树列表
// const GET_DEVICE_LIST = '/api/Publish/GCK/Device/DeviceTree'; //获取设备树列表
const deviceTreeService = {
getDeviceList: {
url: GET_DEVICE_LIST,
method: REQUEST_METHOD_POST,
type: REQUEST_HTTP,
},
};
const dtService = service(deviceTreeService);
const getDeviceList = dtService.getDeviceList;
import DeviceTree from '../index';
const Demo = () => {
const onTreeCheck = (checkedKeysValue) => {
......@@ -33,23 +12,20 @@ const Demo = () => {
return (
<div style={{ width: '200px', height: '400px', border: '1px solid #eee' }}>
<PandaDeviceTree
checkable
onTreeCheck={onTreeCheck}
onTreeSelect={onTreeSelect}
deviceTreeService={getDeviceList}
serviceParams={{
pageIndex: 1,
pageSize: 20,
// deviceTypes: '二供泵房,二供机组',
getChild: true,
userID: 1,
// queryInfo: '',
// sortFields: '',
// direction: '',
// isTop: true,
}}
// pagination={false}
<DeviceTree
onCheck={(data) => {console.log(data)}}
onSelect={(data) => {console.log(data)}}
checkable={true}
selectable={true}
deviceTypes={'压力表,熊猫水表,二供泵房'}
sortFields={''}
direction={''}
classField={'所属分区'}
getChild={true}
userAccessor={false}
keepChecked
setDeviceList={() => {}}
setSearchStr={() => {}}
/>
</div>
);
......
import React, { useContext, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { Input, Layout, Spin, Tree, ConfigProvider } from 'antd';
// import { SearchOutlined } from '@ant-design/icons';
import React, { useCallback, useEffect, useMemo, useRef, useState, useContext } from 'react';
import PandaEmpty from '@wisdom-components/empty';
import classNames from 'classnames';
import { Spin, Input, Tree, Divider, message, Pagination, ConfigProvider } from 'antd';
import { SearchOutlined } from '@ant-design/icons';
import Empty from '@wisdom-components/empty';
import _ from 'lodash';
import { getEquipmentInfo } from './apis';
import './index.less';
import classnames from 'classnames';
// 生成树的数据格式
const treeDataGenerator = (origin, currentType, ref) => {
if (!origin || !Array.isArray(origin)) return [];
return origin.map((item) => {
const { shortName, deviceName, code, deviceType, children: originChildren } = item;
const title = shortName || deviceName;
const children = treeDataGenerator(originChildren, currentType);
const _width = (ref?.current?.getBoundingClientRect().width || 180) - 24 - 8 - 28; // 24为tree的左侧空白,8为右侧滚动条位置,28是左侧复选框和边距等
return {
...item,
title: (
<div
title={title}
style={{
width: _width,
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
}}
>
{title}
</div>
),
key: code,
children,
disabled: currentType ? !(currentType === deviceType) : false,
};
});
};
// 树结构展开平铺
const flattenTreeData = (treeData) => {
if (!treeData || !Array.isArray(treeData)) return {};
const result = {};
const deep = (treeData, target) => {
treeData &&
treeData.forEach((item) => {
target[item.key] = item;
if (item.children && item.children.length > 0) {
deep(item.children, target);
}
});
};
deep(treeData, result);
return result;
};
// 图层树处理
const convertToTree = (data, name) => {
let obj = {};
let newData = [];
data.forEach((item, index) => {
const state = item[name] || '未知';
if (obj[state]) {
obj[state] = [...obj[state], item];
} else {
obj[state] = [item];
}
});
for (let k in obj) {
newData.push({
title: k,
children: [...obj[k]],
key: k,
selectable: false,
disableCheckbox: true,
checkable: false,
});
expandedKeys.push(k);
}
return newData;
};
let dataList = [];
let expandedKeys = [];
const DeviceTree = (props) => {
const {
prefix,
placeholder,
deviceTreeService,
serviceParams,
onTreeCheck,
onTreeSelect,
pagination,
deviceTypes = '二供泵房,二供机组',
userAccessor = false,
getChild = true,
sortFields = '',
direction = '',
classField = '',
customerName = '',
} = props;
const { getPrefixCls } = useContext(ConfigProvider.ConfigContext);
const prefixCls = getPrefixCls('ec-device-tree');
const [treeData, setTreeData] = useState([]);
const [params, setParams] = useState({});
const [totalCount, setTotalCount] = useState(0);
const [loading, setLoading] = useState(false);
const [expandedKeys, setExpandedKeys] = useState([]);
const cusProps = _.pick(props, ['onSelect', 'onCheck', 'selectable', 'checkable']);
const { onSelect, onCheck, keepChecked, singleType = false, selectable, checkable } = props;
const [data, setData] = useState([]);
const [{ pageIndex, pageSize }, setPagination] = useState({
pageIndex: 1,
pageSize: 100,
});
const [searchs, setSearchs] = useState('');
const [loading, setLoading] = useState(true);
const [hasMore, setHasMore] = useState(true);
const ref = useRef();
const dataRef = useRef({
currentType: '',
preCheckedData: [],
preCheckedKeys: [],
preSelectedData: [],
flatData: {},
});
const { currentType, flatData } = dataRef.current;
const [checkedKeys, setCheckedKeys] = useState([]);
const [selectedKeys, setSelectedKeys] = useState([]);
const [first, setFirst] = useState(true);
useEffect(() => {
setParams(serviceParams);
}, []);
const requestQuery = useMemo(() => {
return {
deviceTypes,
pageIndex,
pageSize,
queryInfo: searchs,
getChild,
userID: userAccessor ? window?.globalConfig?.userInfo?.OID : void 0,
sortFields,
classField,
direction,
customerName,
};
}, [
customerName,
deviceTypes,
direction,
getChild,
pageIndex,
pageSize,
searchs,
sortFields,
classField,
]);
useEffect(() => {
setParams(serviceParams);
}, [serviceParams]);
const handleData = (data) => {
data.map((item) => {
item.title = item.deviceName;
item.key = item.stationID;
item.children = handleData(item.children);
});
return data;
};
const fetchData = (param = {}) => {
setLoading(true);
deviceTreeService(param).then((response) => {
if (response.code === 0) {
const data = response.data
? response.data.list && response.data.list.length > 0
? response.data.list[0].deviceList
: []
: [];
const newData = handleData(data);
const keys = newData.length > 0 ? [newData[0].key] : [];
getEquipmentInfo(requestQuery)
.then((res) => {
setLoading(false);
setTreeData(newData);
setTotalCount(response.data.totalCount);
onTreeCheck(newData.length > 0 ? [newData[0]] : []);
onTreeSelect(newData.length > 0 ? [newData[0]] : []);
setCheckedKeys(keys);
setExpandedKeys(keys);
setSelectedKeys(keys);
} else {
message.error(response.msg);
}
});
};
const { list, pageIndex, totalCount } = res.data || {};
const newData = pageIndex === 1 ? list : [...dataList, ...list]; // 非第一页时合并
useEffect(() => {
if (JSON.stringify(params) !== '{}') {
fetchData(params);
}
}, [params]);
// 是否需要默认选中
if (pageIndex === 1 && (!keepChecked || (keepChecked && !checkedKeys.length))) {
setCheckedKeys(newData.length > 0 ? [newData[0].code] : []);
onCheck?.(newData.length > 0 ? [newData[0]] : []);
const onSearch = (e) => {
if (e.type === 'keydown' || e.target.value === '') {
const param = { ...params, queryInfo: e.target.value };
setParams(param);
}
};
// 仅单选时使用select相关数据
!checkable && onSelect?.(newData.length > 0 ? [newData[0]] : []);
!checkable && setSelectedKeys(newData.length > 0 ? [newData[0].code] : []);
// 选中复选框
const onCheck = (checkedKeysValue) => {
const { checked } = checkedKeysValue;
const checkedTree = [];
treeData.forEach((item) => {
if (checked.includes(item.key)) {
checkedTree.push(item);
}
if (item.children.length > 0) {
item.children.forEach((child) => {
if (checked.includes(child.key)) {
checkedTree.push(child);
}
singleType && (dataRef.current.currentType = newData[0].deviceType);
}
setData(() => {
dataList = [...newData];
return newData;
});
}
});
setCheckedKeys(checked);
onTreeCheck(checkedTree);
};
props.setDeviceList(newData);
props.setSearchStr(searchs);
setHasMore(newData.length < totalCount);
})
.catch((err) => {
setLoading(false);
});
}, [requestQuery]);
const onSelect = (selectedKeysValue, info) => {
setSelectedKeys(selectedKeysValue);
onTreeSelect(info.selectedNodes);
};
const onScroll = useCallback(() => {
if (loading || !hasMore) return;
const { clientHeight, scrollHeight, scrollTop } = ref.current;
if (clientHeight + scrollTop + 10 >= scrollHeight) {
// 加载更多
setPagination({
pageSize,
pageIndex: pageIndex + 1,
});
}
}, [loading, pageIndex, pageSize, hasMore]);
const onExpand = (expandedKeysValue) => {
setExpandedKeys(expandedKeysValue);
const onSearch = useCallback(
(value) => {
setSearchs(value);
setPagination({
pageIndex: 1,
pageSize,
});
},
[pageSize],
);
useEffect(() => {
ref.current?.addEventListener('scroll', onScroll, true);
return () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
ref.current?.removeEventListener('scroll', onScroll, true);
};
}, [onScroll]);
const treeData = useMemo(() => {
const treeData = treeDataGenerator(data, singleType && currentType, ref);
const flatData = flattenTreeData(treeData);
dataRef.current.flatData = Object.assign(dataRef.current.flatData || {}, flatData);
return convertToTree(treeData, requestQuery.classField ? 'deviceClass' : 'deviceType');
}, [data, singleType, currentType]);
const handleCheck = (keys, info) => {
// keys中会保留多余的key
const { checked } = keys;
setCheckedKeys(checked);
const data = checked.map((key) => flatData[key]);
onCheck?.(data);
singleType && data.length === 0 && (dataRef.current.currentType = '');
singleType && data.length === 1 && (dataRef.current.currentType = data[0].deviceType);
};
const onPaginationChange = (page) => {
setParams({ ...params, pageIndex: page });
const handleSelect = (keys, info) => {
if (checkable) {
setCheckedKeys(keys);
const data = keys.map((key) => flatData[key]);
onCheck?.(data);
singleType && data.length === 0 && (dataRef.current.currentType = '');
singleType && data.length === 1 && (dataRef.current.currentType = data[0].deviceType);
} else {
setSelectedKeys(keys);
const data = keys.map((key) => flatData[key]);
onSelect?.(data);
}
};
return (
<div className={classNames(prefixCls)}>
<Input
prefix={prefix}
placeholder={placeholder}
bordered={false}
onChange={onSearch}
onPressEnter={onSearch}
/>
<Divider className={classNames(`${prefixCls}-divider`)} />
<div className={classNames(`${prefixCls}-content`)}>
<div className={classNames(prefixCls, 'wkt-scroll-light')}>
<Input.Search placeholder="搜索设备名称" onSearch={onSearch} allowClear />
<div className={classnames(`${prefixCls}-tree-wrap`, 'wkt-scroll-light-plus')} ref={ref}>
<Spin spinning={loading}>
{!!treeData.length && (
{data && data.length ? (
<Tree
checkStrictly
checkedKeys={checkedKeys}
expandedKeys={expandedKeys}
selectedKeys={selectedKeys}
autoExpandParent
treeData={treeData}
onExpand={onExpand}
onCheck={onCheck}
onSelect={onSelect}
checkStrictly
{...props}
defaultExpandParent
defaultExpandedKeys={expandedKeys}
{...cusProps}
onCheck={handleCheck}
onSelect={handleSelect}
/>
) : (
<PandaEmpty />
)}
{!treeData.length && !loading && <Empty />}
</Spin>
</div>
{pagination && (
<div className={classNames(`${prefixCls}-pagination`)}>
<Pagination
simple
hideOnSinglePage
current={params.pageIndex || 1}
pageSize={params.pageSize || 20}
total={totalCount}
onChange={onPaginationChange}
/>
</div>
)}
{!pagination && (
<Divider plain className={classNames(`${prefixCls}-total`)}>
{totalCount} 记录
</Divider>
)}
</div>
);
};
DeviceTree.defaultProps = {
prefix: <SearchOutlined />,
placeholder: '搜索设备名称',
pagination: true,
serviceParams: {},
onTreeCheck: () => {},
onTreeSelect: () => {},
deviceTypes: '二供泵房',
userAccessor: false,
getChild: false,
sortFields: '',
direction: '',
customerName: '',
classField: '',
selectable: false,
checkable: false,
keepChecked: false,
onSelect: () => {},
onCheck: () => {},
setDeviceList: () => {},
setSearchStr: () => {},
};
DeviceTree.propTypes = {
prefix: PropTypes.node,
placeholder: PropTypes.string,
pagination: PropTypes.bool,
serviceParams: PropTypes.object,
onTreeCheck: PropTypes.func,
onTreeSelect: PropTypes.func,
deviceTreeService: PropTypes.any,
deviceTypes: PropTypes.string,
userAccessor: PropTypes.bool,
getChild: PropTypes.bool,
sortFields: PropTypes.string,
direction: PropTypes.string,
customerName: PropTypes.string,
classField: PropTypes.string,
selectable: PropTypes.bool,
checkable: PropTypes.bool,
keepChecked: PropTypes.bool,
onSelect: PropTypes.func,
onCheck: PropTypes.func,
setDeviceList: PropTypes.func,
setSearchStr: PropTypes.func,
};
export default DeviceTree;
......@@ -3,70 +3,18 @@
@ec-device-tree-prefix-cls: ~'@{ant-prefix}-ec-device-tree';
.@{ec-device-tree-prefix-cls} {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
padding: 5px;
overflow: hidden;
padding: 10px;
&-divider {
margin: 6px 0 12px 0;
&-tree-wrap {
height: calc(100% - 48px);
overflow: auto;
}
.@{ant-prefix}-tree-checkbox {
margin: 7px 2px 0 0;
:global {
.@{ant-prefix}-input-search {
margin-bottom: 16px;
}
}
.@{ant-prefix}-tree-title {
white-space: nowrap;
}
.@{ant-prefix}-tree-treenode {
width: 100%;
}
.@{ant-prefix}-tree-switcher {
line-height: 30px;
}
.@{ant-prefix}-tree-switcher-noop {
width: 14px;
}
.@{ant-prefix}-tree-node-content-wrapper {
flex: 1;
min-height: 30px;
overflow: hidden;
line-height: 30px;
text-overflow: ellipsis;
border-radius: 3px;
}
.@{ant-prefix}-tree .@{ant-prefix}-tree-node-content-wrapper.@{ant-prefix}-tree-node-selected {
color: #1890ff;
background-color: #eaf1fe;
}
&-content {
flex: 1;
overflow-y: scroll;
}
.@{ant-prefix}-spin-nested-loading {
height: 100%;
}
&-pagination {
margin: 10px auto;
}
&-total.@{ant-prefix}-divider-horizontal.@{ant-prefix}-divider-with-text {
margin: 10px auto;
color: #a4b1c7;
}
.@{ant-prefix}-divider-horizontal.@{ant-prefix}-divider-with-text::before,
.@{ant-prefix}-divider-horizontal.@{ant-prefix}-divider-with-text::after {
top: 0;
}
}
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment