fix(ui): prevent-duplicate-clicks not work

This commit is contained in:
ReaJason
2026-02-08 20:44:00 +08:00
parent 841909fa64
commit d1782309c1
2 changed files with 55 additions and 39 deletions
+18 -9
View File
@@ -1,7 +1,7 @@
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { HomeLayout } from "fumadocs-ui/layouts/home"; import { HomeLayout } from "fumadocs-ui/layouts/home";
import { LoaderCircle, WandSparklesIcon } from "lucide-react"; import { LoaderCircle, WandSparklesIcon } from "lucide-react";
import { useCallback, useState, useTransition } from "react"; import { useCallback, useRef, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -89,7 +89,7 @@ export default function MemShellPage() {
}); });
const { t } = useTranslation(["common", "memshell"]); const { t } = useTranslation(["common", "memshell"]);
const form = useForm({ const form = useForm<MemShellFormSchema>({
resolver: useYupValidationResolver(memShellFormSchema, t), resolver: useYupValidationResolver(memShellFormSchema, t),
defaultValues, defaultValues,
}); });
@@ -100,7 +100,7 @@ export default function MemShellPage() {
>(); >();
const [generateResult, setGenerateResult] = useState<MemShellResult>(); const [generateResult, setGenerateResult] = useState<MemShellResult>();
const [packMethod, setPackMethod] = useState<string>(""); const [packMethod, setPackMethod] = useState<string>("");
const [isActionPending, startTransition] = useTransition(); const submitLockRef = useRef(false);
const submitMemShell = useCallback( const submitMemShell = useCallback(
async (data: MemShellFormSchema) => { async (data: MemShellFormSchema) => {
@@ -134,10 +134,15 @@ export default function MemShellPage() {
); );
const onSubmit = useCallback( const onSubmit = useCallback(
(data: MemShellFormSchema) => { async (data: MemShellFormSchema) => {
startTransition(() => { if (submitLockRef.current) return;
void submitMemShell(data); submitLockRef.current = true;
});
try {
await submitMemShell(data);
} finally {
submitLockRef.current = false;
}
}, },
[submitMemShell], [submitMemShell],
); );
@@ -156,8 +161,12 @@ export default function MemShellPage() {
form={form} form={form}
/> />
<PackageConfigCard packerConfig={packerConfig} form={form} /> <PackageConfigCard packerConfig={packerConfig} form={form} />
<Button className="w-full" type="submit" disabled={isActionPending}> <Button
{isActionPending ? ( className="w-full"
type="submit"
disabled={form.formState.isSubmitting}
>
{form.formState.isSubmitting ? (
<LoaderCircle className="animate-spin" /> <LoaderCircle className="animate-spin" />
) : ( ) : (
<WandSparklesIcon /> <WandSparklesIcon />
+37 -30
View File
@@ -1,7 +1,7 @@
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { HomeLayout } from "fumadocs-ui/layouts/home"; import { HomeLayout } from "fumadocs-ui/layouts/home";
import { LoaderCircle, WandSparklesIcon } from "lucide-react"; import { LoaderCircle, WandSparklesIcon } from "lucide-react";
import { useState, useTransition } from "react"; import { useRef, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -68,38 +68,41 @@ export default function ProbeShellGenerator() {
>(); >();
const [generateResult, setGenerateResult] = useState<ProbeShellResult>(); const [generateResult, setGenerateResult] = useState<ProbeShellResult>();
const [packMethod, setPackMethod] = useState<string>(""); const [packMethod, setPackMethod] = useState<string>("");
const [isActionPending, startTransition] = useTransition(); const submitLockRef = useRef(false);
const onSubmit = async (data: ProbeShellFormSchema) => { const onSubmit = async (data: ProbeShellFormSchema) => {
startTransition(async () => { if (submitLockRef.current) return;
try { submitLockRef.current = true;
const postData = transformToProbePostData(data);
const response = await fetch(`${env.API_URL}/api/probe/generate`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(postData),
});
if (!response.ok) { try {
const json: APIErrorResponse = await response.json(); const postData = transformToProbePostData(data);
toast.error(t("toast.generateError", { error: json.error })); const response = await fetch(`${env.API_URL}/api/probe/generate`, {
return; method: "POST",
} headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(postData),
});
const result = (await response.json()) as ProbeShellGenerateResponse; if (!response.ok) {
setGenerateResult(result.probeShellResult); const json: APIErrorResponse = await response.json();
setPackResult(result.packResult); toast.error(t("toast.generateError", { error: json.error }));
setAllPackResults(result.allPackResults); return;
setPackMethod(data.packingMethod);
toast.success(t("toast.generateSuccess"));
} catch (error) {
toast.error(
t("toast.generateError", { error: (error as Error).message }),
);
} }
});
const result = (await response.json()) as ProbeShellGenerateResponse;
setGenerateResult(result.probeShellResult);
setPackResult(result.packResult);
setAllPackResults(result.allPackResults);
setPackMethod(data.packingMethod);
toast.success(t("toast.generateSuccess"));
} catch (error) {
toast.error(
t("toast.generateError", { error: (error as Error).message }),
);
} finally {
submitLockRef.current = false;
}
}; };
return ( return (
<HomeLayout {...baseOptions()} links={siteConfig.navLinks}> <HomeLayout {...baseOptions()} links={siteConfig.navLinks}>
@@ -111,8 +114,12 @@ export default function ProbeShellGenerator() {
<div className="w-full xl:w-1/2 flex flex-col gap-2"> <div className="w-full xl:w-1/2 flex flex-col gap-2">
<MainConfigCard form={form} servers={serverConfig} /> <MainConfigCard form={form} servers={serverConfig} />
<PackageConfigCard form={form} packerConfig={packerConfig} /> <PackageConfigCard form={form} packerConfig={packerConfig} />
<Button className="w-full" type="submit" disabled={isActionPending}> <Button
{isActionPending ? ( className="w-full"
type="submit"
disabled={form.formState.isSubmitting}
>
{form.formState.isSubmitting ? (
<LoaderCircle className="animate-spin" /> <LoaderCircle className="animate-spin" />
) : ( ) : (
<WandSparklesIcon /> <WandSparklesIcon />