diff --git a/src/components/base/base-dialog.tsx b/src/components/base/base-dialog.tsx index 219b5108..6b6648fc 100644 --- a/src/components/base/base-dialog.tsx +++ b/src/components/base/base-dialog.tsx @@ -31,22 +31,23 @@ export interface DialogRef { close: () => void; } -export const BaseDialog: React.FC = (props) => { - const { - open, - title, - children, - okBtn, - cancelBtn, - contentSx, - disableCancel, - disableOk, - disableFooter, - loading, - } = props; - +export const BaseDialog: React.FC = ({ + open, + title, + children, + okBtn, + cancelBtn, + contentSx, + disableCancel, + disableOk, + disableFooter, + loading, + onOk, + onCancel, + onClose, +}) => { return ( - + {title} {children} @@ -54,16 +55,12 @@ export const BaseDialog: React.FC = (props) => { {!disableFooter && ( {!disableCancel && ( - )} {!disableOk && ( - + {okBtn} )} diff --git a/src/components/base/base-error-boundary.tsx b/src/components/base/base-error-boundary.tsx index 2475a2ff..8b94429b 100644 --- a/src/components/base/base-error-boundary.tsx +++ b/src/components/base/base-error-boundary.tsx @@ -20,10 +20,8 @@ interface Props { children?: ReactNode; } -export const BaseErrorBoundary = (props: Props) => { +export const BaseErrorBoundary = ({ children }: Props) => { return ( - - {props.children} - + {children} ); }; diff --git a/src/components/base/base-fieldset.tsx b/src/components/base/base-fieldset.tsx index 8374897d..57ffa82a 100644 --- a/src/components/base/base-fieldset.tsx +++ b/src/components/base/base-fieldset.tsx @@ -9,30 +9,36 @@ type Props = { children?: React.ReactNode; }; -export const BaseFieldset: React.FC = (props: Props) => { +export const BaseFieldset: React.FC = ({ + label, + fontSize, + width, + padding, + children, +}: Props) => { const Fieldset = styled(Box)<{ component?: string }>(() => ({ position: "relative", border: "1px solid #bbb", borderRadius: "5px", - width: props.width ?? "auto", - padding: props.padding ?? "15px", + width: width ?? "auto", + padding: padding ?? "15px", })); const Label = styled("legend")(({ theme }) => ({ position: "absolute", top: "-10px", - left: props.padding ?? "15px", + left: padding ?? "15px", backgroundColor: theme.palette.background.paper, backgroundImage: "linear-gradient(rgba(255, 255, 255, 0.16), rgba(255, 255, 255, 0.16))", color: theme.palette.text.primary, - fontSize: props.fontSize ?? "1em", + fontSize: fontSize ?? "1em", })); return (
- - {props.children} + + {children}
); }; diff --git a/src/components/base/base-search-box.tsx b/src/components/base/base-search-box.tsx index 84f0a83b..4ce93a30 100644 --- a/src/components/base/base-search-box.tsx +++ b/src/components/base/base-search-box.tsx @@ -1,6 +1,13 @@ import { Box, SvgIcon, TextField, styled } from "@mui/material"; import Tooltip from "@mui/material/Tooltip"; -import { ChangeEvent, useEffect, useMemo, useRef, useState } from "react"; +import { + ChangeEvent, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; import { useTranslation } from "react-i18next"; import matchCaseIcon from "@/assets/image/component/match_case.svg?react"; @@ -35,15 +42,20 @@ const StyledTextField = styled(TextField)(({ theme }) => ({ }, })); -export const BaseSearchBox = (props: SearchProps) => { +export const BaseSearchBox = ({ + placeholder, + matchCase: defaultMatchCase = false, + matchWholeWord: defaultMatchWholeWord = false, + useRegularExpression: defaultUseRegularExpression = false, + onSearch, +}: SearchProps) => { const { t } = useTranslation(); const inputRef = useRef(null); - const [matchCase, setMatchCase] = useState(props.matchCase ?? false); - const [matchWholeWord, setMatchWholeWord] = useState( - props.matchWholeWord ?? false, - ); + const onSearchRef = useRef(onSearch); + const [matchCase, setMatchCase] = useState(defaultMatchCase); + const [matchWholeWord, setMatchWholeWord] = useState(defaultMatchWholeWord); const [useRegularExpression, setUseRegularExpression] = useState( - props.useRegularExpression ?? false, + defaultUseRegularExpression, ); const [errorMessage, setErrorMessage] = useState(""); @@ -56,8 +68,8 @@ export const BaseSearchBox = (props: SearchProps) => { inheritViewBox: true, }; - // 验证正则表达式的辅助函数 - const validateRegex = (pattern: string) => { + // Helper that verifies whether a pattern is a valid regular expression + const validateRegex = useCallback((pattern: string) => { if (!pattern) return true; try { new RegExp(pattern); @@ -66,46 +78,48 @@ export const BaseSearchBox = (props: SearchProps) => { console.warn("[BaseSearchBox] validateRegex error:", e); return false; } - }; + }, []); const createMatcher = useMemo(() => { return (searchText: string) => { - try { - // 当启用正则表达式验证是否合规 - if (useRegularExpression && searchText) { - const isValid = validateRegex(searchText); - if (!isValid) { - throw new Error(t("Invalid regular expression")); - } + if (useRegularExpression && searchText) { + const isValid = validateRegex(searchText); + if (!isValid) { + // Invalid regex should result in no match + return () => false; + } + } + + return (content: string) => { + if (!searchText) { + return true; } - return (content: string) => { - if (!searchText) return true; + const item = !matchCase ? content.toLowerCase() : content; + const searchItem = !matchCase ? searchText.toLowerCase() : searchText; - const item = !matchCase ? content.toLowerCase() : content; - const searchItem = !matchCase ? searchText.toLowerCase() : searchText; + if (useRegularExpression) { + return new RegExp(searchItem).test(item); + } - if (useRegularExpression) { - return new RegExp(searchItem).test(item); - } + if (matchWholeWord) { + return new RegExp(`\\b${searchItem}\\b`).test(item); + } - if (matchWholeWord) { - return new RegExp(`\\b${searchItem}\\b`).test(item); - } - - return item.includes(searchItem); - }; - } catch (err) { - setErrorMessage(err instanceof Error ? err.message : `${err}`); - return () => false; // 无效正则规则 不匹配值 - } + return item.includes(searchItem); + }; }; - }, [matchCase, matchWholeWord, useRegularExpression, t]); + }, [matchCase, matchWholeWord, useRegularExpression, validateRegex]); + + useEffect(() => { + onSearchRef.current = onSearch; + }, [onSearch]); useEffect(() => { if (!inputRef.current) return; const value = inputRef.current.value; - props.onSearch(createMatcher(value), { + const matcher = createMatcher(value); + onSearchRef.current(matcher, { text: value, matchCase, matchWholeWord, @@ -117,7 +131,7 @@ export const BaseSearchBox = (props: SearchProps) => { const value = e.target?.value ?? ""; setErrorMessage(""); - // 验证正则表达式 + // Validate regex input eagerly if (useRegularExpression && value) { const isValid = validateRegex(value); if (!isValid) { @@ -125,7 +139,8 @@ export const BaseSearchBox = (props: SearchProps) => { } } - props.onSearch(createMatcher(value), { + const matcher = createMatcher(value); + onSearchRef.current(matcher, { text: value, matchCase, matchWholeWord, @@ -133,6 +148,21 @@ export const BaseSearchBox = (props: SearchProps) => { }); }; + const handleToggleUseRegularExpression = () => { + setUseRegularExpression((prev) => { + const next = !prev; + if (!next) { + setErrorMessage(""); + } else { + const value = inputRef.current?.value ?? ""; + if (value && !validateRegex(value)) { + setErrorMessage(t("Invalid regular expression")); + } + } + return next; + }); + }; + return ( { size="small" variant="outlined" spellCheck="false" - placeholder={props.placeholder ?? t("Filter conditions")} + placeholder={placeholder ?? t("Filter conditions")} sx={{ input: { py: 0.65, px: 1.25 } }} onChange={onChange} error={!!errorMessage} @@ -158,7 +188,7 @@ export const BaseSearchBox = (props: SearchProps) => { component={matchCaseIcon} {...iconStyle} aria-label={matchCase ? "active" : "inactive"} - onClick={() => setMatchCase(!matchCase)} + onClick={() => setMatchCase((prev) => !prev)} /> @@ -168,7 +198,7 @@ export const BaseSearchBox = (props: SearchProps) => { component={matchWholeWordIcon} {...iconStyle} aria-label={matchWholeWord ? "active" : "inactive"} - onClick={() => setMatchWholeWord(!matchWholeWord)} + onClick={() => setMatchWholeWord((prev) => !prev)} /> @@ -178,10 +208,8 @@ export const BaseSearchBox = (props: SearchProps) => { component={useRegularExpressionIcon} aria-label={useRegularExpression ? "active" : "inactive"} {...iconStyle} - onClick={() => - setUseRegularExpression(!useRegularExpression) - } - />{" "} + onClick={handleToggleUseRegularExpression} + /> diff --git a/src/hooks/useServiceInstaller.ts b/src/hooks/useServiceInstaller.ts index 612da388..b074c678 100644 --- a/src/hooks/useServiceInstaller.ts +++ b/src/hooks/useServiceInstaller.ts @@ -3,6 +3,7 @@ import { useCallback } from "react"; import { installService, restartCore } from "@/services/cmds"; import { showNotice } from "@/services/noticeService"; + import { useSystemState } from "./use-system-state"; const executeWithErrorHandling = async ( @@ -34,6 +35,8 @@ export const useServiceInstaller = () => { ); await executeWithErrorHandling(() => restartCore(), "Restarting Core..."); + await mutateRunningMode(); + await mutateServiceOk(); }, [mutateRunningMode, mutateServiceOk]); return { installServiceAndRestartCore }; }; diff --git a/src/hooks/useServiceUninstaller.ts b/src/hooks/useServiceUninstaller.ts index 03e52508..bb2c450d 100644 --- a/src/hooks/useServiceUninstaller.ts +++ b/src/hooks/useServiceUninstaller.ts @@ -37,6 +37,8 @@ export const useServiceUninstaller = () => { ); await executeWithErrorHandling(() => restartCore(), "Restarting Core..."); + await mutateRunningMode(); + await mutateServiceOk(); }, [mutateRunningMode, mutateServiceOk]); return { uninstallServiceAndRestartCore };