组选择加上对结果的操作

This commit is contained in:
2026-05-18 17:18:22 +08:00
parent 704bc34625
commit 4efa3b7ac3
4 changed files with 109 additions and 13 deletions

View File

@@ -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',

View File

@@ -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': '禁用',

View File

@@ -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',

View File

@@ -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