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

fix: 修改设备树业务组件

parent de5c1917
...@@ -28,12 +28,33 @@ group: ...@@ -28,12 +28,33 @@ group:
api 参考 Antd Tree 组件 https://ant.design/components/tree-cn/ api 参考 Antd Tree 组件 https://ant.design/components/tree-cn/
| 参数 | 说明 | 类型 | 默认值 | | 参数 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- | | ------------- | -------------------------------- | --------------------- | -------- |
| prefix | 搜索框的前置图标 | ReactNode | 搜索 icon | | deviceTypes | 设备类型, 多种设备逗号隔开 | string | 二供泵房 |
| placeholder | 搜索框占位符 | string | 搜索设备名称 | | userAccessor | 是否启用设备权限过滤 | boolean | false |
| checkable | 节点前添加 Checkbox 复选框 | boolean | false | | getChild | 是否查询子设备 | boolean | false |
| pagination | 默认分页,false 取消分页 | boolean | true | | sortFields | 设备排序字段, 设备台账表中的字段 | string | - |
| serviceParams | 服务参数 | object | { pageIndex: 1, pageSize: 500, deviceTypes: '二供泵房,二供机组', getChild: true, userID: 1, queryInfo: '', sortFields: '', direction: '', isTop: true } | | direction | 设备排序方向, asc/desc | string | - |
| deviceTreeService `必需` | 设备树服务 | promise | - | | classField | 分组字段,设备台账表中的字段 | string | - |
| onTreeCheck | 点击复选框触发 | function(checkedNodes){ } | - | | customerName | 服务参数 | string | - |
| onTreeSelect | 点击树节点触发 | function(selectedNodes){ } | - | | 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 React from 'react';
import PandaDeviceTree from '../index'; import DeviceTree 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;
const Demo = () => { const Demo = () => {
const onTreeCheck = (checkedKeysValue) => { const onTreeCheck = (checkedKeysValue) => {
...@@ -33,23 +12,20 @@ const Demo = () => { ...@@ -33,23 +12,20 @@ const Demo = () => {
return ( return (
<div style={{ width: '200px', height: '400px', border: '1px solid #eee' }}> <div style={{ width: '200px', height: '400px', border: '1px solid #eee' }}>
<PandaDeviceTree <DeviceTree
checkable onCheck={(data) => {console.log(data)}}
onTreeCheck={onTreeCheck} onSelect={(data) => {console.log(data)}}
onTreeSelect={onTreeSelect} checkable={true}
deviceTreeService={getDeviceList} selectable={true}
serviceParams={{ deviceTypes={'压力表,熊猫水表,二供泵房'}
pageIndex: 1, sortFields={''}
pageSize: 20, direction={''}
// deviceTypes: '二供泵房,二供机组', classField={'所属分区'}
getChild: true, getChild={true}
userID: 1, userAccessor={false}
// queryInfo: '', keepChecked
// sortFields: '', setDeviceList={() => {}}
// direction: '', setSearchStr={() => {}}
// isTop: true,
}}
// pagination={false}
/> />
</div> </div>
); );
......
import React, { useContext, useEffect, useState } from 'react';
import PropTypes from 'prop-types'; 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 classNames from 'classnames';
import { Spin, Input, Tree, Divider, message, Pagination, ConfigProvider } from 'antd'; import _ from 'lodash';
import { SearchOutlined } from '@ant-design/icons'; import { getEquipmentInfo } from './apis';
import Empty from '@wisdom-components/empty';
import './index.less'; 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 DeviceTree = (props) => {
const { const {
prefix, deviceTypes = '二供泵房,二供机组',
placeholder, userAccessor = false,
deviceTreeService, getChild = true,
serviceParams, sortFields = '',
onTreeCheck, direction = '',
onTreeSelect, classField = '',
pagination, customerName = '',
} = props; } = props;
const { getPrefixCls } = useContext(ConfigProvider.ConfigContext); const { getPrefixCls } = useContext(ConfigProvider.ConfigContext);
const prefixCls = getPrefixCls('ec-device-tree'); const prefixCls = getPrefixCls('ec-device-tree');
const [treeData, setTreeData] = useState([]); const cusProps = _.pick(props, ['onSelect', 'onCheck', 'selectable', 'checkable']);
const [params, setParams] = useState({}); const { onSelect, onCheck, keepChecked, singleType = false, selectable, checkable } = props;
const [totalCount, setTotalCount] = useState(0);
const [loading, setLoading] = useState(false);
const [expandedKeys, setExpandedKeys] = useState([]);
const [checkedKeys, setCheckedKeys] = useState([]);
const [selectedKeys, setSelectedKeys] = useState([]);
useEffect(() => { const [data, setData] = useState([]);
setParams(serviceParams); 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();
useEffect(() => { const dataRef = useRef({
setParams(serviceParams); currentType: '',
}, [serviceParams]); preCheckedData: [],
preCheckedKeys: [],
const handleData = (data) => { preSelectedData: [],
data.map((item) => { flatData: {},
item.title = item.deviceName;
item.key = item.stationID;
item.children = handleData(item.children);
}); });
return data; const { currentType, flatData } = dataRef.current;
const [checkedKeys, setCheckedKeys] = useState([]);
const [selectedKeys, setSelectedKeys] = useState([]);
const [first, setFirst] = useState(true);
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,
]);
const fetchData = (param = {}) => { useEffect(() => {
setLoading(true); setLoading(true);
deviceTreeService(param).then((response) => { getEquipmentInfo(requestQuery)
if (response.code === 0) { .then((res) => {
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] : [];
setLoading(false); setLoading(false);
setTreeData(newData); const { list, pageIndex, totalCount } = res.data || {};
setTotalCount(response.data.totalCount); const newData = pageIndex === 1 ? list : [...dataList, ...list]; // 非第一页时合并
onTreeCheck(newData.length > 0 ? [newData[0]] : []);
onTreeSelect(newData.length > 0 ? [newData[0]] : []);
setCheckedKeys(keys);
setExpandedKeys(keys);
setSelectedKeys(keys);
} else {
message.error(response.msg);
}
});
};
useEffect(() => { // 是否需要默认选中
if (JSON.stringify(params) !== '{}') { if (pageIndex === 1 && (!keepChecked || (keepChecked && !checkedKeys.length))) {
fetchData(params); setCheckedKeys(newData.length > 0 ? [newData[0].code] : []);
} onCheck?.(newData.length > 0 ? [newData[0]] : []);
}, [params]);
const onSearch = (e) => { // 仅单选时使用select相关数据
if (e.type === 'keydown' || e.target.value === '') { !checkable && onSelect?.(newData.length > 0 ? [newData[0]] : []);
const param = { ...params, queryInfo: e.target.value }; !checkable && setSelectedKeys(newData.length > 0 ? [newData[0].code] : []);
setParams(param);
}
};
// 选中复选框 singleType && (dataRef.current.currentType = newData[0].deviceType);
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);
} }
setData(() => {
dataList = [...newData];
return newData;
});
props.setDeviceList(newData);
props.setSearchStr(searchs);
setHasMore(newData.length < totalCount);
})
.catch((err) => {
setLoading(false);
});
}, [requestQuery]);
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 onSearch = useCallback(
(value) => {
setSearchs(value);
setPagination({
pageIndex: 1,
pageSize,
}); });
setCheckedKeys(checked); },
onTreeCheck(checkedTree); [pageSize],
}; );
const onSelect = (selectedKeysValue, info) => { useEffect(() => {
setSelectedKeys(selectedKeysValue); ref.current?.addEventListener('scroll', onScroll, true);
onTreeSelect(info.selectedNodes); return () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
ref.current?.removeEventListener('scroll', onScroll, true);
}; };
}, [onScroll]);
const onExpand = (expandedKeysValue) => { const treeData = useMemo(() => {
setExpandedKeys(expandedKeysValue); 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) => { const handleSelect = (keys, info) => {
setParams({ ...params, pageIndex: page }); 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 ( return (
<div className={classNames(prefixCls)}> <div className={classNames(prefixCls, 'wkt-scroll-light')}>
<Input <Input.Search placeholder="搜索设备名称" onSearch={onSearch} allowClear />
prefix={prefix} <div className={classnames(`${prefixCls}-tree-wrap`, 'wkt-scroll-light-plus')} ref={ref}>
placeholder={placeholder}
bordered={false}
onChange={onSearch}
onPressEnter={onSearch}
/>
<Divider className={classNames(`${prefixCls}-divider`)} />
<div className={classNames(`${prefixCls}-content`)}>
<Spin spinning={loading}> <Spin spinning={loading}>
{!!treeData.length && ( {data && data.length ? (
<Tree <Tree
checkStrictly
checkedKeys={checkedKeys} checkedKeys={checkedKeys}
expandedKeys={expandedKeys}
selectedKeys={selectedKeys} selectedKeys={selectedKeys}
autoExpandParent
treeData={treeData} treeData={treeData}
onExpand={onExpand} defaultExpandParent
onCheck={onCheck} defaultExpandedKeys={expandedKeys}
onSelect={onSelect} {...cusProps}
checkStrictly onCheck={handleCheck}
{...props} onSelect={handleSelect}
/> />
) : (
<PandaEmpty />
)} )}
{!treeData.length && !loading && <Empty />}
</Spin> </Spin>
</div> </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> </div>
); );
}; };
DeviceTree.defaultProps = { DeviceTree.defaultProps = {
prefix: <SearchOutlined />, deviceTypes: '二供泵房',
placeholder: '搜索设备名称', userAccessor: false,
pagination: true, getChild: false,
serviceParams: {}, sortFields: '',
onTreeCheck: () => {}, direction: '',
onTreeSelect: () => {}, customerName: '',
classField: '',
selectable: false,
checkable: false,
keepChecked: false,
onSelect: () => {},
onCheck: () => {},
setDeviceList: () => {},
setSearchStr: () => {},
}; };
DeviceTree.propTypes = { DeviceTree.propTypes = {
prefix: PropTypes.node, deviceTypes: PropTypes.string,
placeholder: PropTypes.string, userAccessor: PropTypes.bool,
pagination: PropTypes.bool, getChild: PropTypes.bool,
serviceParams: PropTypes.object, sortFields: PropTypes.string,
onTreeCheck: PropTypes.func, direction: PropTypes.string,
onTreeSelect: PropTypes.func, customerName: PropTypes.string,
deviceTreeService: PropTypes.any, 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; export default DeviceTree;
...@@ -3,70 +3,18 @@ ...@@ -3,70 +3,18 @@
@ec-device-tree-prefix-cls: ~'@{ant-prefix}-ec-device-tree'; @ec-device-tree-prefix-cls: ~'@{ant-prefix}-ec-device-tree';
.@{ec-device-tree-prefix-cls} { .@{ec-device-tree-prefix-cls} {
display: flex;
flex-direction: column;
width: 100%;
height: 100%; height: 100%;
padding: 5px;
&-divider {
margin: 6px 0 12px 0;
}
.@{ant-prefix}-tree-checkbox {
margin: 7px 2px 0 0;
}
.@{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; overflow: hidden;
line-height: 30px; padding: 10px;
text-overflow: ellipsis;
border-radius: 3px;
}
.@{ant-prefix}-tree .@{ant-prefix}-tree-node-content-wrapper.@{ant-prefix}-tree-node-selected { &-tree-wrap {
color: #1890ff; height: calc(100% - 48px);
background-color: #eaf1fe; overflow: auto;
} }
&-content { :global {
flex: 1; .@{ant-prefix}-input-search {
overflow-y: scroll; margin-bottom: 16px;
}
.@{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