diff --git a/Changelog.md b/Changelog.md index 83c6e5cfb..f8f8801f3 100644 --- a/Changelog.md +++ b/Changelog.md @@ -20,6 +20,7 @@ - 修复侧边栏可能的未能正确跳转 - 修复解锁测试部分地区图标编码不正确 - 修复 IP 检测切页后强制刷新,改为仅在必要时更新 +- 修复在搜索框输入不完整正则直接崩溃
✨ 新增功能 diff --git a/src/components/base/base-search-box.tsx b/src/components/base/base-search-box.tsx index beb961e0c..47cd57011 100644 --- a/src/components/base/base-search-box.tsx +++ b/src/components/base/base-search-box.tsx @@ -50,6 +50,20 @@ export const BaseSearchBox = ({ onSearch, }: SearchProps) => { const { t } = useTranslation(); + + const escapeRegex = useCallback((value: string) => { + return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + }, []); + + const buildRegex = useCallback((pattern: string, flags = "") => { + try { + return new RegExp(pattern, flags); + } catch (e) { + console.warn("[BaseSearchBox] buildRegex error:", e); + return null; + } + }, []); + const inputRef = useRef(null); const onSearchRef = useRef(onSearch); const [matchCase, setMatchCase] = useState(defaultMatchCase); @@ -69,47 +83,63 @@ export const BaseSearchBox = ({ }; // Helper that verifies whether a pattern is a valid regular expression - const validateRegex = useCallback((pattern: string) => { - if (!pattern) return true; - try { - new RegExp(pattern); - return true; - } catch (e) { - console.warn("[BaseSearchBox] validateRegex error:", e); - return false; - } - }, []); + const validateRegex = useCallback( + (pattern: string, flags = "") => { + if (!pattern) return true; + return !!buildRegex(pattern, flags); + }, + [buildRegex], + ); const createMatcher = useMemo(() => { return (searchText: string) => { - if (useRegularExpression && searchText) { - const isValid = validateRegex(searchText); - if (!isValid) { - // Invalid regex should result in no match - return () => false; - } + if (!searchText) { + return () => true; + } + + const flags = matchCase ? "" : "i"; + + if (useRegularExpression) { + const regex = buildRegex(searchText, flags); + if (!regex) return () => false; + + return (content: string) => { + try { + return regex.test(content); + } catch (e) { + console.warn("[BaseSearchBox] regex match error:", e); + return false; + } + }; + } + + if (matchWholeWord) { + const regex = buildRegex(`\\b${escapeRegex(searchText)}\\b`, flags); + if (!regex) return () => false; + + return (content: string) => { + try { + return regex.test(content); + } catch (e) { + console.warn("[BaseSearchBox] whole word match error:", e); + return false; + } + }; } return (content: string) => { - if (!searchText) { - return true; - } - - const item = !matchCase ? content.toLowerCase() : content; - const searchItem = !matchCase ? searchText.toLowerCase() : searchText; - - if (useRegularExpression) { - return new RegExp(searchItem).test(item); - } - - if (matchWholeWord) { - return new RegExp(`\\b${searchItem}\\b`).test(item); - } - - return item.includes(searchItem); + const item = matchCase ? content : content.toLowerCase(); + const target = matchCase ? searchText : searchText.toLowerCase(); + return item.includes(target); }; }; - }, [matchCase, matchWholeWord, useRegularExpression, validateRegex]); + }, [ + buildRegex, + escapeRegex, + matchCase, + matchWholeWord, + useRegularExpression, + ]); useEffect(() => { onSearchRef.current = onSearch; @@ -130,10 +160,11 @@ export const BaseSearchBox = ({ const onChange = (e: ChangeEvent) => { const value = e.target?.value ?? ""; setErrorMessage(""); + const flags = matchCase ? "" : "i"; // Validate regex input eagerly if (useRegularExpression && value) { - const isValid = validateRegex(value); + const isValid = validateRegex(value, flags); if (!isValid) { setErrorMessage(t("shared.validation.invalidRegex")); } @@ -155,7 +186,8 @@ export const BaseSearchBox = ({ setErrorMessage(""); } else { const value = inputRef.current?.value ?? ""; - if (value && !validateRegex(value)) { + const flags = matchCase ? "" : "i"; + if (value && !validateRegex(value, flags)) { setErrorMessage(t("shared.validation.invalidRegex")); } }