feat: add search result actions to symbol select

This commit is contained in:
2026-05-18 17:42:02 +08:00
parent 4efa3b7ac3
commit 331e4ff625

View File

@@ -1,5 +1,6 @@
import { ProFormTreeSelect } from '@ant-design/pro-components'; 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 useUserInfo from '@/hooks/useUserInfo';
import Loading from '@/loading'; import Loading from '@/loading';
import { getSymbolTreeList } from '@/utils/dataCenter'; import { getSymbolTreeList } from '@/utils/dataCenter';
@@ -13,6 +14,37 @@ interface SymbolSelectProps {
disabled?: boolean; disabled?: boolean;
} }
const normalizeSearchText = (value: string) =>
value?.trim().toLowerCase() || '';
const isNodeDefaultSearchMatched = (
node: API.SymbolTreeNode,
searchText: string,
) =>
String(node.value || '')
.toLowerCase()
.includes(searchText);
const collectSearchResultLeafKeys = (
nodes: API.SymbolTreeNode[],
searchText: string,
parentMatched = false,
): string[] => {
if (!searchText) return [];
return nodes.flatMap((node) => {
const matched =
parentMatched || isNodeDefaultSearchMatched(node, searchText);
const children = node.children || [];
if (children.length === 0) {
return matched ? [node.key] : [];
}
return collectSearchResultLeafKeys(children, searchText, matched);
});
};
const SymbolSelect: React.FC<SymbolSelectProps> = ({ const SymbolSelect: React.FC<SymbolSelectProps> = ({
dataSourceId, dataSourceId,
value, value,
@@ -20,17 +52,29 @@ const SymbolSelect: React.FC<SymbolSelectProps> = ({
}) => { }) => {
const { userInfo } = useUserInfo(); const { userInfo } = useUserInfo();
const companyInfo = userInfo?.company; const companyInfo = userInfo?.company;
const [treeData, setTreeData] = useState<any[]>([]); const [treeData, setTreeData] = useState<API.SymbolTreeNode[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
// Internally maintains a keys array for TreeSelect display // Internally maintains a keys array for TreeSelect display
const [checkedKeys, setCheckedKeys] = useState<string[]>([]); 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) => { const loadData = async (force: boolean = false) => {
setLoading(true); setLoading(true);
try { try {
const data = await getSymbolTreeList(companyInfo, dataSourceId, force); const data = await getSymbolTreeList(companyInfo, dataSourceId, force);
setCheckedKeys([]); setCheckedKeys([]);
setSearchValue('');
setTreeData(data); setTreeData(data);
} finally { } finally {
setLoading(false); setLoading(false);
@@ -56,20 +100,36 @@ const SymbolSelect: React.FC<SymbolSelectProps> = ({
}, [value, treeData]); }, [value, treeData]);
// 3. When users manually check/uncheck items, convert internal keys back to string and trigger onChange // 3. When users manually check/uncheck items, convert internal keys back to string and trigger onChange
const handleTreeChange = (keys: any) => { const updateCheckedKeys = (keys: string[]) => {
// Handle the object format that antd tree may return
const actualKeys = Array.isArray(keys) ? keys : keys.checked;
// Update internal UI state // Update internal UI state
setCheckedKeys(actualKeys); setCheckedKeys(keys);
// Generate string to pass to external Form // Generate string to pass to external Form
if (onChange) { if (onChange) {
const patternString = generatePattern(treeData, actualKeys); const patternString = generatePattern(treeData, keys);
onChange(patternString); 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 (!dataSourceId) return null;
if (loading) return <Loading padding="0" />; if (loading) return <Loading padding="0" />;
@@ -83,10 +143,44 @@ const SymbolSelect: React.FC<SymbolSelectProps> = ({
listHeight: 410, listHeight: 410,
treeData: treeData, treeData: treeData,
treeCheckable: true, treeCheckable: true,
onSearch: setSearchValue,
value: checkedKeys, // Use the internally parsed array value: checkedKeys, // Use the internally parsed array
onChange: handleTreeChange, onChange: handleTreeChange,
maxTagCount: 'responsive', maxTagCount: 'responsive',
virtual: true, 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" style={{ fontSize: 12 }}>
{$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: { fieldNames: {
label: 'title', label: 'title',
value: 'key', value: 'key',