feat: 接口选择改为支持搜索的下拉组件,按模块分组显示

This commit is contained in:
jason
2026-03-13 02:29:40 +08:00
parent b6a98ddaba
commit e3f7583c40
6 changed files with 416 additions and 11 deletions

View File

@@ -32,6 +32,10 @@ const ApiDebugger = ({ onTitleChange, storageKey = 'tab_default' }) => {
const [currentMatchIndex, setCurrentMatchIndex] = useState(0);
const [matchCount, setMatchCount] = useState(0);
// Endpoint search state
const [endpointSearchQuery, setEndpointSearchQuery] = useState('');
const [isEndpointDropdownOpen, setIsEndpointDropdownOpen] = useState(false);
// UX state
const [copyResText, setCopyResText] = useState('复制结果');
const [copyCurlText, setCopyCurlText] = useState('复制 CURL');
@@ -102,7 +106,16 @@ const ApiDebugger = ({ onTitleChange, storageKey = 'tab_default' }) => {
};
fetchInitialData();
}, []);
// Close dropdown when clicking outside
const handleClickOutside = (e) => {
if (isEndpointDropdownOpen && !e.target.closest('.form-group')) {
setIsEndpointDropdownOpen(false);
}
};
document.addEventListener('click', handleClickOutside);
return () => document.removeEventListener('click', handleClickOutside);
}, [isEndpointDropdownOpen]);
useEffect(() => {
const tenant = tenants.find(t => t.id === parseInt(selectedTenantId));
@@ -428,10 +441,11 @@ const ApiDebugger = ({ onTitleChange, storageKey = 'tab_default' }) => {
setQueryParamsError('');
};
const handleEndpointChange = (e) => {
const endpointId = Number(e.target.value);
const handleEndpointChange = (endpointId) => {
setSelectedEndpointId(endpointId);
localStorage.setItem(`${storageKey}_endpointId`, endpointId);
setIsEndpointDropdownOpen(false);
setEndpointSearchQuery('');
const endpoint = endpoints.find(ep => ep.id === endpointId);
if (endpoint) {
@@ -452,6 +466,28 @@ const ApiDebugger = ({ onTitleChange, storageKey = 'tab_default' }) => {
}
};
// Filter endpoints by search query
const searchFilteredEndpoints = useMemo(() => {
if (!endpointSearchQuery.trim()) return filteredEndpoints;
const query = endpointSearchQuery.toLowerCase();
return filteredEndpoints.filter(ep =>
ep.name.toLowerCase().includes(query) ||
ep.method.toLowerCase().includes(query) ||
(ep.module && ep.module.toLowerCase().includes(query))
);
}, [filteredEndpoints, endpointSearchQuery]);
// Group endpoints by module
const groupedEndpoints = useMemo(() => {
const groups = {};
searchFilteredEndpoints.forEach(ep => {
const module = ep.module || '其他';
if (!groups[module]) groups[module] = [];
groups[module].push(ep);
});
return groups;
}, [searchFilteredEndpoints]);
return (
<div style={{ width: '100%', maxWidth: '1600px', margin: '0 auto', height: '100%', display: 'flex', flexDirection: 'column' }}>
<header style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '2rem' }}>
@@ -470,12 +506,130 @@ const ApiDebugger = ({ onTitleChange, storageKey = 'tab_default' }) => {
</div>
<div className="form-group">
<label>选择接口</label>
<select value={selectedEndpointId} onChange={handleEndpointChange}>
<option value="">-- 请选择接口 --</option>
{filteredEndpoints
.map(ep => <option key={ep.id} value={ep.id}>[{ep.method}] {ep.name}</option>)
}
</select>
<div style={{ position: 'relative' }}>
<div
onClick={() => setIsEndpointDropdownOpen(!isEndpointDropdownOpen)}
style={{
padding: '0.5rem',
border: '1px solid var(--border)',
borderRadius: '4px',
cursor: 'pointer',
backgroundColor: 'var(--bg)',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}
>
<span style={{ color: selectedEndpointId ? 'inherit' : '#999' }}>
{selectedEndpointId
? `[${endpoints.find(e => e.id === parseInt(selectedEndpointId))?.method}] ${endpoints.find(e => e.id === parseInt(selectedEndpointId))?.name}`
: '-- 请选择接口 --'
}
</span>
<span style={{ fontSize: '0.75rem' }}></span>
</div>
{isEndpointDropdownOpen && (
<div
style={{
position: 'absolute',
top: '100%',
left: 0,
right: 0,
marginTop: '4px',
backgroundColor: 'var(--bg)',
border: '1px solid var(--border)',
borderRadius: '4px',
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
zIndex: 1000,
maxHeight: '400px',
display: 'flex',
flexDirection: 'column'
}}
>
<div style={{ padding: '0.5rem', borderBottom: '1px solid var(--border)', backgroundColor: 'var(--bg)' }}>
<input
type="text"
placeholder="搜索接口名称、方法或分类..."
value={endpointSearchQuery}
onChange={(e) => setEndpointSearchQuery(e.target.value)}
onClick={(e) => e.stopPropagation()}
style={{
width: '100%',
padding: '0.5rem',
border: '1px solid var(--border)',
borderRadius: '4px',
fontSize: '0.875rem'
}}
autoFocus
/>
</div>
<div style={{ overflowY: 'auto', maxHeight: '340px' }}>
{Object.keys(groupedEndpoints).length === 0 ? (
<div style={{ padding: '1rem', textAlign: 'center', color: '#999' }}>
未找到匹配的接口
</div>
) : (
Object.entries(groupedEndpoints).map(([module, eps]) => (
<div key={module}>
<div style={{
padding: '0.5rem',
backgroundColor: 'var(--bg)',
fontWeight: 'bold',
fontSize: '0.875rem',
position: 'sticky',
top: 0,
zIndex: 2,
borderBottom: '1px solid var(--border)'
}}>
{module}
</div>
{eps.map(ep => (
<div
key={ep.id}
onClick={() => handleEndpointChange(ep.id)}
style={{
padding: '0.5rem 1rem',
cursor: 'pointer',
backgroundColor: selectedEndpointId === ep.id ? 'rgba(59, 130, 246, 0.1)' : 'transparent',
borderLeft: selectedEndpointId === ep.id ? '3px solid #3b82f6' : '3px solid transparent'
}}
onMouseEnter={(e) => {
if (selectedEndpointId !== ep.id) {
e.currentTarget.style.backgroundColor = 'rgba(0,0,0,0.05)';
}
}}
onMouseLeave={(e) => {
if (selectedEndpointId !== ep.id) {
e.currentTarget.style.backgroundColor = 'transparent';
}
}}
>
<div style={{ fontSize: '0.875rem' }}>
<span style={{
display: 'inline-block',
padding: '2px 6px',
backgroundColor: ep.method === 'POST' ? '#10b981' : ep.method === 'GET' ? '#3b82f6' : '#f59e0b',
color: 'white',
borderRadius: '3px',
fontSize: '0.75rem',
fontWeight: 'bold',
marginRight: '0.5rem',
minWidth: '45px',
textAlign: 'center'
}}>
{ep.method}
</span>
{ep.name}
</div>
</div>
))}
</div>
))
)}
</div>
</div>
)}
</div>
</div>
<div className="form-group">
<label>接口地址 (URL)</label>