组选择加上对结果的操作
This commit is contained in:
@@ -144,6 +144,9 @@ export default {
|
||||
'pages.rules.allGroups': 'All Groups',
|
||||
'pages.rules.allSymbols': 'All Symbols',
|
||||
'pages.rules.allAssets': 'All Assets',
|
||||
'pages.rules.searchResults': 'Search results',
|
||||
'pages.rules.selectSearchResults': 'Select results',
|
||||
'pages.rules.deselectSearchResults': 'Deselect results',
|
||||
'pages.rules.trigger': 'Triggered',
|
||||
'pages.rules.triggeredCount': 'Times',
|
||||
'pages.rules.disable': 'Disable',
|
||||
|
||||
@@ -142,6 +142,9 @@ export default {
|
||||
'pages.rules.allGroups': '全部组',
|
||||
'pages.rules.allSymbols': '全部产品',
|
||||
'pages.rules.allAssets': '全部资产',
|
||||
'pages.rules.searchResults': '搜索结果',
|
||||
'pages.rules.selectSearchResults': '全选结果',
|
||||
'pages.rules.deselectSearchResults': '取消结果',
|
||||
'pages.rules.trigger': '触发',
|
||||
'pages.rules.triggeredCount': '次数',
|
||||
'pages.rules.disable': '禁用',
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { ProFormTreeSelect } from '@ant-design/pro-components';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Button, Space, Typography } from 'antd';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import useUserInfo from '@/hooks/useUserInfo';
|
||||
import Loading from '@/loading';
|
||||
import { getGroupTreeList } from '@/utils/dataCenter';
|
||||
import { generatePattern2, parsePatternToKeys2 } from '@/utils/groupUtil';
|
||||
import {
|
||||
type GroupTreeNode,
|
||||
generatePattern2,
|
||||
parsePatternToKeys2,
|
||||
} from '@/utils/groupUtil';
|
||||
import { $t } from '@/utils/i18n';
|
||||
|
||||
interface GroupSelectProps {
|
||||
@@ -13,6 +18,31 @@ interface GroupSelectProps {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const normalizeSearchText = (value: string) =>
|
||||
value?.trim().toLowerCase() || '';
|
||||
|
||||
const isNodeTitleMatched = (node: GroupTreeNode, searchText: string) =>
|
||||
node.title.toLowerCase().includes(searchText);
|
||||
|
||||
const collectSearchResultLeafKeys = (
|
||||
nodes: GroupTreeNode[],
|
||||
searchText: string,
|
||||
parentMatched = false,
|
||||
): string[] => {
|
||||
if (!searchText) return [];
|
||||
|
||||
return nodes.flatMap((node) => {
|
||||
const matched = parentMatched || isNodeTitleMatched(node, searchText);
|
||||
const children = node.children || [];
|
||||
|
||||
if (children.length === 0) {
|
||||
return matched ? [node.key] : [];
|
||||
}
|
||||
|
||||
return collectSearchResultLeafKeys(children, searchText, matched);
|
||||
});
|
||||
};
|
||||
|
||||
const GroupSelect: React.FC<GroupSelectProps> = ({
|
||||
dataSourceId,
|
||||
value,
|
||||
@@ -20,10 +50,21 @@ const GroupSelect: React.FC<GroupSelectProps> = ({
|
||||
}) => {
|
||||
const { userInfo } = useUserInfo();
|
||||
const companyInfo = userInfo?.company;
|
||||
const [treeData, setTreeData] = useState<any[]>([]);
|
||||
const [treeData, setTreeData] = useState<GroupTreeNode[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
// Internally maintains a keys array for TreeSelect display
|
||||
const [checkedKeys, setCheckedKeys] = useState<string[]>([]);
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
|
||||
const normalizedSearchValue = useMemo(
|
||||
() => normalizeSearchText(searchValue),
|
||||
[searchValue],
|
||||
);
|
||||
|
||||
const searchResultKeys = useMemo(
|
||||
() => collectSearchResultLeafKeys(treeData, normalizedSearchValue),
|
||||
[treeData, normalizedSearchValue],
|
||||
);
|
||||
|
||||
const loadData = async (force: boolean = false) => {
|
||||
setLoading(true);
|
||||
@@ -57,20 +98,36 @@ const GroupSelect: React.FC<GroupSelectProps> = ({
|
||||
}, [value, treeData]);
|
||||
|
||||
// 3. When users manually check/uncheck items, convert internal keys back to string and trigger onChange
|
||||
const handleTreeChange = (keys: any) => {
|
||||
// Handle the object format that antd tree may return
|
||||
const actualKeys = Array.isArray(keys) ? keys : keys.checked;
|
||||
|
||||
const updateCheckedKeys = (keys: string[]) => {
|
||||
// Update internal UI state
|
||||
setCheckedKeys(actualKeys);
|
||||
setCheckedKeys(keys);
|
||||
|
||||
// Generate string to pass to external Form
|
||||
if (onChange) {
|
||||
const patternString = generatePattern2(treeData, actualKeys);
|
||||
const patternString = generatePattern2(treeData, keys);
|
||||
onChange(patternString);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTreeChange = (keys: any) => {
|
||||
// Handle the object format that antd tree may return
|
||||
const actualKeys = Array.isArray(keys) ? keys : keys?.checked || [];
|
||||
updateCheckedKeys(actualKeys);
|
||||
};
|
||||
|
||||
const handleSelectSearchResults = () => {
|
||||
updateCheckedKeys(
|
||||
Array.from(new Set([...checkedKeys, ...searchResultKeys])),
|
||||
);
|
||||
};
|
||||
|
||||
const handleDeselectSearchResults = () => {
|
||||
const searchResultKeySet = new Set(searchResultKeys);
|
||||
updateCheckedKeys(
|
||||
checkedKeys.filter((checkedKey) => !searchResultKeySet.has(checkedKey)),
|
||||
);
|
||||
};
|
||||
|
||||
if (!dataSourceId) return null;
|
||||
if (loading) return <Loading padding="0" />;
|
||||
|
||||
@@ -84,10 +141,47 @@ const GroupSelect: React.FC<GroupSelectProps> = ({
|
||||
listHeight: 410,
|
||||
treeData: treeData,
|
||||
treeCheckable: true,
|
||||
showSearch: true,
|
||||
searchValue,
|
||||
onSearch: setSearchValue,
|
||||
treeNodeFilterProp: 'title',
|
||||
value: checkedKeys, // Use the internally parsed array
|
||||
onChange: handleTreeChange,
|
||||
maxTagCount: 'responsive',
|
||||
virtual: true,
|
||||
popupRender: (originNode) =>
|
||||
normalizedSearchValue && searchResultKeys.length > 0 ? (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
borderBottom: '1px solid rgba(5, 5, 5, 0.06)',
|
||||
padding: '8px 12px',
|
||||
}}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
>
|
||||
<Space wrap size={8} className="justify-between w-100">
|
||||
<Typography.Text type="secondary">
|
||||
{$t('pages.rules.searchResults')}:{' '}
|
||||
{searchResultKeys.length}
|
||||
</Typography.Text>
|
||||
<div>
|
||||
<Button size="small" onClick={handleSelectSearchResults}>
|
||||
{$t('pages.rules.selectSearchResults')}
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={handleDeselectSearchResults}
|
||||
>
|
||||
{$t('pages.rules.deselectSearchResults')}
|
||||
</Button>
|
||||
</div>
|
||||
</Space>
|
||||
</div>
|
||||
{originNode}
|
||||
</>
|
||||
) : (
|
||||
originNode
|
||||
),
|
||||
fieldNames: {
|
||||
label: 'title',
|
||||
value: 'key',
|
||||
|
||||
4
src/services/typings.d.ts
vendored
4
src/services/typings.d.ts
vendored
@@ -1,6 +1,3 @@
|
||||
// @ts-expect-error
|
||||
/* eslint-disable */
|
||||
|
||||
declare namespace API {
|
||||
// Response data format agreed with the backend
|
||||
interface BaseResponse<T = unknown> {
|
||||
@@ -116,7 +113,6 @@ declare namespace API {
|
||||
fromName: string | null;
|
||||
mailHostConfigId: number | string | null;
|
||||
mailStatus: number | null;
|
||||
special?: number;
|
||||
};
|
||||
|
||||
// "DataSource" Type
|
||||
|
||||
Reference in New Issue
Block a user