feat(设备搜索): 为设备选择器添加防抖搜索功能

This commit is contained in:
zhang1106
2026-03-03 10:51:51 +08:00
parent c36ee75e49
commit ef8f3ab565
4 changed files with 215 additions and 65 deletions
+54 -23
View File
@@ -5,6 +5,18 @@ import axios from 'axios';
const { Option } = Select; const { Option } = Select;
const debounce = (func, wait) => {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
};
const CableCreateModal = ({ visible, onClose, onSuccess, sourceDevice }) => { const CableCreateModal = ({ visible, onClose, onSuccess, sourceDevice }) => {
const [form] = Form.useForm(); const [form] = Form.useForm();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -14,6 +26,30 @@ const CableCreateModal = ({ visible, onClose, onSuccess, sourceDevice }) => {
const [fetchingDevices, setFetchingDevices] = useState(false); const [fetchingDevices, setFetchingDevices] = useState(false);
const prevVisibleRef = useRef(false); const prevVisibleRef = useRef(false);
const fetchDevices = useCallback(async (keyword = '') => {
try {
setFetchingDevices(true);
const params = { pageSize: 50 };
if (keyword && keyword.trim()) {
params.keyword = keyword.trim();
}
const response = await axios.get('/api/devices', { params });
setDevices(response.data.devices || []);
} catch (error) {
console.error('Failed to fetch devices:', error);
message.error('获取设备列表失败');
} finally {
setFetchingDevices(false);
}
}, []);
const handleDeviceSearch = useCallback(
debounce(value => {
fetchDevices(value);
}, 300),
[fetchDevices]
);
// Initialize when visible changes from false to true // Initialize when visible changes from false to true
useEffect(() => { useEffect(() => {
if (visible && !prevVisibleRef.current) { if (visible && !prevVisibleRef.current) {
@@ -27,20 +63,7 @@ const CableCreateModal = ({ visible, onClose, onSuccess, sourceDevice }) => {
fetchDevices(); fetchDevices();
} }
prevVisibleRef.current = visible; prevVisibleRef.current = visible;
}, [visible, sourceDevice, form]); }, [visible, sourceDevice, form, fetchDevices]);
const fetchDevices = async () => {
try {
setFetchingDevices(true);
const response = await axios.get('/api/devices', { params: { pageSize: 1000 } });
setDevices(response.data.devices || []);
} catch (error) {
console.error('Failed to fetch devices:', error);
message.error('获取设备列表失败');
} finally {
setFetchingDevices(false);
}
};
const fetchDevicePorts = async (deviceId, type) => { const fetchDevicePorts = async (deviceId, type) => {
if (!deviceId) { if (!deviceId) {
@@ -136,14 +159,18 @@ const CableCreateModal = ({ visible, onClose, onSuccess, sourceDevice }) => {
rules={[{ required: true, message: '请选择源设备' }]} rules={[{ required: true, message: '请选择源设备' }]}
> >
<Select <Select
placeholder="选择源设备" placeholder="输入关键词搜索源设备"
showSearch showSearch
filterOption={(input, option) => filterOption={false}
(option?.children ?? '').toLowerCase().includes(input.toLowerCase()) onSearch={handleDeviceSearch}
} onDropdownVisibleChange={open => {
if (open && devices.length === 0) {
fetchDevices();
}
}}
onChange={handleSourceDeviceChange} onChange={handleSourceDeviceChange}
loading={fetchingDevices} loading={fetchingDevices}
disabled={!!sourceDevice} // Lock source device if provided disabled={!!sourceDevice}
> >
{devices.map(d => ( {devices.map(d => (
<Option key={d.deviceId} value={d.deviceId}> <Option key={d.deviceId} value={d.deviceId}>
@@ -176,11 +203,15 @@ const CableCreateModal = ({ visible, onClose, onSuccess, sourceDevice }) => {
rules={[{ required: true, message: '请选择目标设备' }]} rules={[{ required: true, message: '请选择目标设备' }]}
> >
<Select <Select
placeholder="选择目标设备" placeholder="输入关键词搜索目标设备"
showSearch showSearch
filterOption={(input, option) => filterOption={false}
(option?.children ?? '').toLowerCase().includes(input.toLowerCase()) onSearch={handleDeviceSearch}
} onDropdownVisibleChange={open => {
if (open && devices.length === 0) {
fetchDevices();
}
}}
onChange={handleTargetDeviceChange} onChange={handleTargetDeviceChange}
loading={fetchingDevices} loading={fetchingDevices}
> >
+53 -20
View File
@@ -57,6 +57,18 @@ const { Panel } = Collapse;
const { Text, Title } = Typography; const { Text, Title } = Typography;
const { TextArea } = Input; const { TextArea } = Input;
const debounce = (func, wait) => {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
};
// 设计令牌 - 现代化配色方案 // 设计令牌 - 现代化配色方案
const designTokens = { const designTokens = {
colors: { colors: {
@@ -171,6 +183,7 @@ const animations = {
function CableManagement() { function CableManagement() {
const [cables, setCables] = useState([]); const [cables, setCables] = useState([]);
const [devices, setDevices] = useState([]); const [devices, setDevices] = useState([]);
const [deviceSearching, setDeviceSearching] = useState(false);
const [switchDevices, setSwitchDevices] = useState([]); const [switchDevices, setSwitchDevices] = useState([]);
const [groupedCables, setGroupedCables] = useState({}); const [groupedCables, setGroupedCables] = useState({});
const [devicePorts, setDevicePorts] = useState({}); const [devicePorts, setDevicePorts] = useState({});
@@ -262,9 +275,14 @@ function CableManagement() {
} }
}, [filters, devicePorts]); }, [filters, devicePorts]);
const fetchDevices = useCallback(async () => { const fetchDevices = useCallback(async (keyword = '') => {
try { try {
const response = await axios.get('/api/devices', { params: { pageSize: 100 } }); setDeviceSearching(true);
const params = { pageSize: 50 };
if (keyword && keyword.trim()) {
params.keyword = keyword.trim();
}
const response = await axios.get('/api/devices', { params });
const allDevices = response.data.devices || []; const allDevices = response.data.devices || [];
const switches = allDevices.filter(device => device.type === 'switch'); const switches = allDevices.filter(device => device.type === 'switch');
setDevices(allDevices); setDevices(allDevices);
@@ -272,9 +290,18 @@ function CableManagement() {
} catch (error) { } catch (error) {
message.error('获取设备列表失败'); message.error('获取设备列表失败');
console.error('获取设备列表失败:', error); console.error('获取设备列表失败:', error);
} finally {
setDeviceSearching(false);
} }
}, []); }, []);
const handleDeviceSearch = useCallback(
debounce(value => {
fetchDevices(value);
}, 300),
[fetchDevices]
);
const fetchDevicePorts = useCallback(async deviceId => { const fetchDevicePorts = useCallback(async deviceId => {
if (!deviceId) { if (!deviceId) {
setDevicePorts(prev => ({ ...prev, [deviceId]: [] })); setDevicePorts(prev => ({ ...prev, [deviceId]: [] }));
@@ -886,17 +913,19 @@ function CableManagement() {
交换机 交换机
</div> </div>
<Select <Select
placeholder="选择交换机" placeholder="输入关键词搜索交换机"
style={{ width: '100%' }} style={{ width: '100%' }}
value={filters.switchDeviceId || undefined} value={filters.switchDeviceId || undefined}
onChange={value => setFilters(prev => ({ ...prev, switchDeviceId: value }))} onChange={value => setFilters(prev => ({ ...prev, switchDeviceId: value }))}
allowClear allowClear
showSearch showSearch
filterOption={(input, option) => { loading={deviceSearching}
const device = switchDevices.find(d => d.deviceId === option.value); filterOption={false}
if (!device) return false; onSearch={handleDeviceSearch}
const searchText = `${device.name} ${device.deviceId}`.toLowerCase(); onDropdownVisibleChange={open => {
return searchText.indexOf(input.toLowerCase()) >= 0; if (open && switchDevices.length === 0) {
fetchDevices();
}
}} }}
suffixIcon={<AppstoreOutlined />} suffixIcon={<AppstoreOutlined />}
> >
@@ -1288,13 +1317,15 @@ function CableManagement() {
rules={[{ required: true, message: '请选择源设备' }]} rules={[{ required: true, message: '请选择源设备' }]}
> >
<Select <Select
placeholder="请选择源设备" placeholder="输入关键词搜索源设备"
showSearch showSearch
filterOption={(input, option) => { loading={deviceSearching}
const device = switchDevices.find(d => d.deviceId === option.value); filterOption={false}
if (!device) return false; onSearch={handleDeviceSearch}
const searchText = `${device.name} ${device.deviceId}`.toLowerCase(); onDropdownVisibleChange={open => {
return searchText.indexOf(input.toLowerCase()) >= 0; if (open && switchDevices.length === 0) {
fetchDevices();
}
}} }}
onChange={value => { onChange={value => {
fetchDevicePorts(value); fetchDevicePorts(value);
@@ -1353,13 +1384,15 @@ function CableManagement() {
rules={[{ required: true, message: '请选择目标设备' }]} rules={[{ required: true, message: '请选择目标设备' }]}
> >
<Select <Select
placeholder="请选择目标设备" placeholder="输入关键词搜索目标设备"
showSearch showSearch
filterOption={(input, option) => { loading={deviceSearching}
const device = devices.find(d => d.deviceId === option.value); filterOption={false}
if (!device) return false; onSearch={handleDeviceSearch}
const searchText = `${device.name} ${device.deviceId}`.toLowerCase(); onDropdownVisibleChange={open => {
return searchText.indexOf(input.toLowerCase()) >= 0; if (open && devices.length === 0) {
fetchDevices();
}
}} }}
onChange={value => { onChange={value => {
fetchDevicePorts(value); fetchDevicePorts(value);
+45 -14
View File
@@ -59,6 +59,18 @@ const { Panel } = Collapse;
const { Text, Title } = Typography; const { Text, Title } = Typography;
const { TextArea } = Input; const { TextArea } = Input;
const debounce = (func, wait) => {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
};
// 设计令牌 - 与接线管理页面保持一致 // 设计令牌 - 与接线管理页面保持一致
const designTokens = { const designTokens = {
colors: { colors: {
@@ -149,6 +161,7 @@ const animations = {
function PortManagement() { function PortManagement() {
const [ports, setPorts] = useState([]); const [ports, setPorts] = useState([]);
const [devices, setDevices] = useState([]); const [devices, setDevices] = useState([]);
const [deviceSearching, setDeviceSearching] = useState(false);
const [cables, setCables] = useState([]); const [cables, setCables] = useState([]);
const [groupedPorts, setGroupedPorts] = useState({}); const [groupedPorts, setGroupedPorts] = useState({});
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -217,16 +230,30 @@ function PortManagement() {
} }
}, [filters]); }, [filters]);
const fetchDevices = useCallback(async () => { const fetchDevices = useCallback(async (keyword = '') => {
try { try {
const response = await axios.get('/api/devices', { params: { pageSize: 100 } }); setDeviceSearching(true);
const params = { pageSize: 50 };
if (keyword && keyword.trim()) {
params.keyword = keyword.trim();
}
const response = await axios.get('/api/devices', { params });
setDevices(response.data.devices || response.data || []); setDevices(response.data.devices || response.data || []);
} catch (error) { } catch (error) {
message.error('获取设备列表失败'); message.error('获取设备列表失败');
console.error('获取设备列表失败:', error); console.error('获取设备列表失败:', error);
} finally {
setDeviceSearching(false);
} }
}, []); }, []);
const handleDeviceSearch = useCallback(
debounce(value => {
fetchDevices(value);
}, 300),
[fetchDevices]
);
const fetchCables = useCallback(async () => { const fetchCables = useCallback(async () => {
try { try {
const response = await axios.get('/api/cables'); const response = await axios.get('/api/cables');
@@ -772,17 +799,19 @@ function PortManagement() {
设备 设备
</div> </div>
<Select <Select
placeholder="选择设备" placeholder="输入关键词搜索设备"
style={{ width: '100%' }} style={{ width: '100%' }}
value={filters.deviceId || undefined} value={filters.deviceId || undefined}
onChange={value => setFilters(prev => ({ ...prev, deviceId: value }))} onChange={value => setFilters(prev => ({ ...prev, deviceId: value }))}
allowClear allowClear
showSearch showSearch
filterOption={(input, option) => { loading={deviceSearching}
const device = devices.find(d => d.deviceId === option.value); filterOption={false}
if (!device) return false; onSearch={handleDeviceSearch}
const searchText = `${device.name} ${device.deviceId}`.toLowerCase(); onDropdownVisibleChange={open => {
return searchText.indexOf(input.toLowerCase()) >= 0; if (open && devices.length === 0) {
fetchDevices();
}
}} }}
> >
{devices.map(device => ( {devices.map(device => (
@@ -1222,13 +1251,15 @@ function PortManagement() {
rules={[{ required: true, message: '请选择设备' }]} rules={[{ required: true, message: '请选择设备' }]}
> >
<Select <Select
placeholder="请选择设备" placeholder="输入关键词搜索设备"
showSearch showSearch
filterOption={(input, option) => { loading={deviceSearching}
const device = devices.find(d => d.deviceId === option.value); filterOption={false}
if (!device) return false; onSearch={handleDeviceSearch}
const searchText = `${device.name} ${device.deviceId}`.toLowerCase(); onDropdownVisibleChange={open => {
return searchText.indexOf(input.toLowerCase()) >= 0; if (open && devices.length === 0) {
fetchDevices();
}
}} }}
> >
{devices.map(device => ( {devices.map(device => (
+63 -8
View File
@@ -46,6 +46,18 @@ import axios from 'axios';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
const debounce = (func, wait) => {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
};
// 安全地从 localStorage 获取用户信息 // 安全地从 localStorage 获取用户信息
const getUserFromStorage = () => { const getUserFromStorage = () => {
try { try {
@@ -305,15 +317,32 @@ function TicketManagement() {
[searchFilters] [searchFilters]
); );
const fetchDevices = useCallback(async () => { const [deviceSearching, setDeviceSearching] = useState(false);
const [deviceSearchValue, setDeviceSearchValue] = useState('');
const fetchDevices = useCallback(async (keyword = '') => {
try { try {
const response = await axios.get('/api/devices', { params: { pageSize: 100 } }); setDeviceSearching(true);
const params = { pageSize: 50 };
if (keyword && keyword.trim()) {
params.keyword = keyword.trim();
}
const response = await axios.get('/api/devices', { params });
setDevices(response.data.devices || []); setDevices(response.data.devices || []);
} catch (error) { } catch (error) {
console.error('获取设备列表失败:', error); console.error('获取设备列表失败:', error);
} finally {
setDeviceSearching(false);
} }
}, []); }, []);
const handleDeviceSearch = useCallback(
debounce(value => {
fetchDevices(value);
}, 300),
[fetchDevices]
);
const fetchCategories = useCallback(async () => { const fetchCategories = useCallback(async () => {
try { try {
const response = await axios.get('/api/ticket-categories'); const response = await axios.get('/api/ticket-categories');
@@ -448,10 +477,23 @@ function TicketManagement() {
break; break;
case 'device': case 'device':
formItem = ( formItem = (
<Select placeholder="选择设备" showSearch optionFilterProp="children"> <Select
placeholder="输入关键词搜索设备(序列号/名称/IP等)"
showSearch
allowClear
loading={deviceSearching}
filterOption={false}
onSearch={handleDeviceSearch}
notFoundContent={deviceSearching ? '搜索中...' : '请输入关键词搜索'}
onDropdownVisibleChange={open => {
if (open && devices.length === 0) {
fetchDevices();
}
}}
>
{devices.map(device => ( {devices.map(device => (
<Option key={device.deviceId} value={device.deviceId}> <Option key={device.deviceId} value={device.deviceId}>
{device.name} - {device.serialNumber} {device.name} {device.serialNumber ? `- ${device.serialNumber}` : ''}
</Option> </Option>
))} ))}
</Select> </Select>
@@ -467,7 +509,7 @@ function TicketManagement() {
</Form.Item> </Form.Item>
); );
}, },
[devices] [devices, deviceSearching, handleDeviceSearch, fetchDevices]
); );
const tableColumns = useMemo(() => { const tableColumns = useMemo(() => {
@@ -766,10 +808,23 @@ function TicketManagement() {
if (deviceSource === 'select') { if (deviceSource === 'select') {
return ( return (
<Form.Item name="deviceId" label="关联设备" rules={[{ required: false }]}> <Form.Item name="deviceId" label="关联设备" rules={[{ required: false }]}>
<Select placeholder="选择设备" showSearch optionFilterProp="children" allowClear> <Select
placeholder="输入关键词搜索设备(序列号/名称/IP等)"
showSearch
allowClear
loading={deviceSearching}
filterOption={false}
onSearch={handleDeviceSearch}
notFoundContent={deviceSearching ? '搜索中...' : '请输入关键词搜索'}
onDropdownVisibleChange={open => {
if (open && devices.length === 0) {
fetchDevices();
}
}}
>
{devices.map(device => ( {devices.map(device => (
<Option key={device.deviceId} value={device.deviceId}> <Option key={device.deviceId} value={device.deviceId}>
{device.name} - {device.serialNumber} {device.name} {device.serialNumber ? `- ${device.serialNumber}` : ''}
</Option> </Option>
))} ))}
</Select> </Select>
@@ -794,7 +849,7 @@ function TicketManagement() {
</Form.Item> </Form.Item>
</> </>
); );
}, [deviceSource, devices]); }, [deviceSource, devices, deviceSearching, handleDeviceSearch, fetchDevices]);
const renderFormItems = useCallback(() => { const renderFormItems = useCallback(() => {
const items = []; const items = [];