From fa54cbc0d155082e1f16b29cbb06c18f8cbe935f Mon Sep 17 00:00:00 2001
From: haitaoo <haitao@sifou.com>
Date: Fri, 21 Jul 2023 18:15:52 +0800
Subject: [PATCH] feat: Complete **SPAM** blocking of critical data apis.

---
 ui/src/common/interface.ts                    |  36 ++-
 ui/src/components/Actions/index.tsx           |  59 ++--
 ui/src/components/Comment/index.tsx           | 169 ++++++++----
 ui/src/components/Operate/index.tsx           |  65 +++--
 ui/src/components/Unactivate/index.tsx        |  91 ++-----
 ui/src/hooks/index.ts                         |   2 +
 ui/src/hooks/useCaptchaModal/index.tsx        | 253 ++++++++++++++++++
 ui/src/hooks/useReportModal/index.tsx         |  42 ++-
 ui/src/pages/Questions/Ask/index.tsx          | 110 ++++----
 .../components/InviteToAnswer/index.tsx       |  25 +-
 .../Detail/components/WriteAnswer/index.tsx   |  60 +++--
 ui/src/pages/Questions/EditAnswer/index.tsx   |  62 +++--
 ui/src/pages/Search/index.tsx                 |  58 +++-
 .../AccountForgot/components/sendEmail.tsx    | 140 ++++------
 .../ChangeEmail/components/sendEmail.tsx      | 136 ++++------
 ui/src/pages/Users/Login/index.tsx            |  78 ++----
 .../Register/components/SignUpForm/index.tsx  |  72 ++---
 .../Account/components/ModifyEmail/index.tsx  |  66 +----
 .../Account/components/ModifyPass/index.tsx   |  56 +---
 ui/src/services/client/question.ts            |   7 +-
 ui/src/services/client/search.ts              |  20 +-
 ui/src/services/common.ts                     |  61 +++--
 ui/src/utils/common.ts                        |   7 +-
 ui/src/utils/floppyNavigation.ts              |  60 +++--
 ui/src/utils/request.ts                       |   4 +
 25 files changed, 970 insertions(+), 769 deletions(-)
 create mode 100644 ui/src/hooks/useCaptchaModal/index.tsx

diff --git a/ui/src/common/interface.ts b/ui/src/common/interface.ts
index 151a8c39..e5d78bb3 100644
--- a/ui/src/common/interface.ts
+++ b/ui/src/common/interface.ts
@@ -9,6 +9,11 @@ export interface FormDataType {
   [prop: string]: FormValue;
 }
 
+export interface FieldError {
+  error_field: string;
+  error_msg: string;
+}
+
 export interface Paging {
   page: number;
   page_size?: number;
@@ -52,7 +57,7 @@ export interface TagInfo extends TagBase {
   main_tag_slug_name?: string;
   excerpt?;
 }
-export interface QuestionParams {
+export interface QuestionParams extends ImgCodeReq{
   title: string;
   url_title?: string;
   content: string;
@@ -68,7 +73,7 @@ export interface ListResult<T = any> {
   list: T[];
 }
 
-export interface AnswerParams {
+export interface AnswerParams extends ImgCodeReq {
   content: string;
   html: string;
   question_id: string;
@@ -169,10 +174,29 @@ export interface PasswordResetReq extends ImgCodeReq {
   e_mail: string;
 }
 
-export interface CheckImgReq {
-  action: 'login' | 'e_mail' | 'find_pass' | 'modify_pass';
+export interface PasswordReplaceReq extends ImgCodeReq {
+  code: string;
+  pass: string;
 }
 
+export interface CaptchaReq extends ImgCodeReq {
+  verify: ImgCodeRes['verify'];
+}
+
+export type CaptchaKey =
+  | 'email'
+  | 'password'
+  | 'edit_userinfo'
+  | 'question'
+  | 'answer'
+  | 'comment'
+  | 'edit'
+  | 'invitation_answer'
+  | 'search'
+  | 'report'
+  | 'delete'
+  | 'vote';
+
 export interface SetNoticeReq {
   notice_switch: boolean;
 }
@@ -222,7 +246,7 @@ export interface AnswerItem {
   [prop: string]: any;
 }
 
-export interface PostAnswerReq {
+export interface PostAnswerReq extends ImgCodeReq {
   content: string;
   html?: string;
   question_id: string;
@@ -425,7 +449,7 @@ export interface FollowParams {
 /**
  * @description search request params
  */
-export interface SearchParams {
+export interface SearchParams extends ImgCodeReq {
   q: string;
   order: string;
   page: number;
diff --git a/ui/src/components/Actions/index.tsx b/ui/src/components/Actions/index.tsx
index 554f57c3..dfa2102b 100644
--- a/ui/src/components/Actions/index.tsx
+++ b/ui/src/components/Actions/index.tsx
@@ -6,9 +6,10 @@ import classNames from 'classnames';
 
 import { Icon } from '@/components';
 import { loggedUserInfoStore } from '@/stores';
-import { useToast } from '@/hooks';
+import { useToast, useCaptchaModal } from '@/hooks';
 import { tryNormalLogged } from '@/utils/guard';
 import { bookmark, postVote } from '@/services';
+import * as Types from '@/common/interface';
 
 interface Props {
   className?: string;
@@ -36,6 +37,8 @@ const Index: FC<Props> = ({ className, data, source }) => {
   const { username = '' } = loggedUserInfoStore((state) => state.user);
   const toast = useToast();
   const { t } = useTranslation();
+  const vCaptcha = useCaptchaModal('vote');
+
   useEffect(() => {
     if (data) {
       setVotes(data.votesCount);
@@ -61,27 +64,39 @@ const Index: FC<Props> = ({ className, data, source }) => {
       return;
     }
     const isCancel = (type === 'up' && like) || (type === 'down' && hate);
-    postVote(
-      {
-        object_id: data?.id,
-        is_cancel: isCancel,
-      },
-      type,
-    )
-      .then((res) => {
-        setVotes(res.votes);
-        setLike(res.vote_status === 'vote_up');
-        setHated(res.vote_status === 'vote_down');
-      })
-      .catch((err) => {
-        const errMsg = err?.value;
-        if (errMsg) {
-          toast.onShow({
-            msg: errMsg,
-            variant: 'danger',
-          });
-        }
-      });
+    vCaptcha.check(() => {
+      const imgCode: Types.ImgCodeReq = {
+        captcha_id: undefined,
+        captcha_code: undefined,
+      };
+      vCaptcha.resolveCaptchaReq(imgCode);
+      postVote(
+        {
+          object_id: data?.id,
+          is_cancel: isCancel,
+          ...imgCode,
+        },
+        type,
+      )
+        .then(async (res) => {
+          await vCaptcha.close();
+          setVotes(res.votes);
+          setLike(res.vote_status === 'vote_up');
+          setHated(res.vote_status === 'vote_down');
+        })
+        .catch((err) => {
+          if (err?.isError) {
+            vCaptcha.handleCaptchaError(err.list);
+          }
+          const errMsg = err?.value;
+          if (errMsg) {
+            toast.onShow({
+              msg: errMsg,
+              variant: 'danger',
+            });
+          }
+        });
+    });
   };
 
   const handleBookmark = () => {
diff --git a/ui/src/components/Comment/index.tsx b/ui/src/components/Comment/index.tsx
index 66a5229c..da3f5d84 100644
--- a/ui/src/components/Comment/index.tsx
+++ b/ui/src/components/Comment/index.tsx
@@ -8,7 +8,7 @@ import { unionBy } from 'lodash';
 
 import * as Types from '@/common/interface';
 import { Modal } from '@/components';
-import { usePageUsers, useReportModal } from '@/hooks';
+import { usePageUsers, useReportModal, useCaptchaModal } from '@/hooks';
 import {
   matchedUsers,
   parseUserInfo,
@@ -43,6 +43,11 @@ const Comment = ({ objectId, mode, commentId }) => {
 
   const reportModal = useReportModal();
 
+  const addCaptcha = useCaptchaModal('comment');
+  const editCaptcha = useCaptchaModal('edit');
+  const dCaptcha = useCaptchaModal('delete');
+  const vCaptcha = useCaptchaModal('vote');
+
   const { t } = useTranslation('translation', { keyPrefix: 'comment' });
   const scrollCallback = useCallback((el, co) => {
     if (pageIndex === 0 && co.comment_id === commentId) {
@@ -120,43 +125,67 @@ const Comment = ({ objectId, mode, commentId }) => {
     };
 
     if (item.type === 'edit') {
-      return updateComment({
-        ...params,
-        comment_id: item.comment_id,
-      }).then((res) => {
-        setComments(
-          comments.map((comment) => {
-            if (comment.comment_id === item.comment_id) {
-              comment.showEdit = false;
-              comment.parsed_text = res.parsed_text;
-              comment.original_text = res.original_text;
+      return editCaptcha.check(() => {
+        const up = {
+          ...params,
+          comment_id: item.comment_id,
+          captcha_code: undefined,
+          captcha_id: undefined,
+        };
+        editCaptcha.resolveCaptchaReq(up);
+
+        return updateComment(up)
+          .then(async (res) => {
+            await editCaptcha.close();
+            setComments(
+              comments.map((comment) => {
+                if (comment.comment_id === item.comment_id) {
+                  comment.showEdit = false;
+                  comment.parsed_text = res.parsed_text;
+                  comment.original_text = res.original_text;
+                }
+                return comment;
+              }),
+            );
+          })
+          .catch((err) => {
+            if (err.isError) {
+              editCaptcha.handleCaptchaError(err.list);
             }
-            return comment;
-          }),
-        );
+          });
       });
     }
-    return addComment(params).then((res) => {
-      if (item.type === 'reply') {
-        const index = comments.findIndex(
-          (comment) => comment.comment_id === item.comment_id,
-        );
-        comments[index].showReply = false;
-        comments.splice(index + 1, 0, res);
-        setComments([...comments]);
-      } else {
-        setComments([
-          ...comments.map((comment) => {
-            if (comment.comment_id === item.comment_id) {
-              comment.showReply = false;
-            }
-            return comment;
-          }),
-          res,
-        ]);
-      }
 
-      setVisibleComment(false);
+    return addCaptcha.check(() => {
+      const req = {
+        ...params,
+        captcha_code: undefined,
+        captcha_id: undefined,
+      };
+      addCaptcha.resolveCaptchaReq(req);
+
+      return addComment(req).then((res) => {
+        if (item.type === 'reply') {
+          const index = comments.findIndex(
+            (comment) => comment.comment_id === item.comment_id,
+          );
+          comments[index].showReply = false;
+          comments.splice(index + 1, 0, res);
+          setComments([...comments]);
+        } else {
+          setComments([
+            ...comments.map((comment) => {
+              if (comment.comment_id === item.comment_id) {
+                comment.showReply = false;
+              }
+              return comment;
+            }),
+            res,
+          ]);
+        }
+
+        setVisibleComment(false);
+      });
     });
   };
 
@@ -167,11 +196,23 @@ const Comment = ({ objectId, mode, commentId }) => {
       confirmBtnVariant: 'danger',
       confirmText: t('delete', { keyPrefix: 'btns' }),
       onConfirm: () => {
-        deleteComment(id).then(() => {
-          if (pageIndex === 0) {
-            mutate();
-          }
-          setComments(comments.filter((item) => item.comment_id !== id));
+        dCaptcha.check(() => {
+          const imgCode = { captcha_id: undefined, captcha_code: undefined };
+          dCaptcha.resolveCaptchaReq(imgCode);
+
+          deleteComment(id, imgCode)
+            .then(async () => {
+              await dCaptcha.close();
+              if (pageIndex === 0) {
+                mutate();
+              }
+              setComments(comments.filter((item) => item.comment_id !== id));
+            })
+            .catch((ex) => {
+              if (ex.isError) {
+                dCaptcha.handleCaptchaError(ex.list);
+              }
+            });
         });
       },
     });
@@ -182,24 +223,40 @@ const Comment = ({ objectId, mode, commentId }) => {
       return;
     }
 
-    postVote(
-      {
-        object_id: id,
-        is_cancel,
-      },
-      'up',
-    ).then(() => {
-      setComments(
-        comments.map((item) => {
-          if (item.comment_id === id) {
-            item.vote_count = is_cancel
-              ? item.vote_count - 1
-              : item.vote_count + 1;
-            item.is_vote = !is_cancel;
+    vCaptcha.check(() => {
+      const imgCode: Types.ImgCodeReq = {
+        captcha_id: undefined,
+        captcha_code: undefined,
+      };
+      vCaptcha.resolveCaptchaReq(imgCode);
+
+      postVote(
+        {
+          object_id: id,
+          is_cancel,
+          ...imgCode,
+        },
+        'up',
+      )
+        .then(async () => {
+          await vCaptcha.close();
+          setComments(
+            comments.map((item) => {
+              if (item.comment_id === id) {
+                item.vote_count = is_cancel
+                  ? item.vote_count - 1
+                  : item.vote_count + 1;
+                item.is_vote = !is_cancel;
+              }
+              return item;
+            }),
+          );
+        })
+        .catch((ex) => {
+          if (ex.isError) {
+            vCaptcha.handleCaptchaError(ex.list);
           }
-          return item;
-        }),
-      );
+        });
     });
   };
 
diff --git a/ui/src/components/Operate/index.tsx b/ui/src/components/Operate/index.tsx
index 03e150f6..d322b9d1 100644
--- a/ui/src/components/Operate/index.tsx
+++ b/ui/src/components/Operate/index.tsx
@@ -4,7 +4,7 @@ import { Link, useNavigate } from 'react-router-dom';
 import { useTranslation } from 'react-i18next';
 
 import { Modal } from '@/components';
-import { useReportModal, useToast } from '@/hooks';
+import { useReportModal, useToast, useCaptchaModal } from '@/hooks';
 import { QuestionOperationReq } from '@/common/interface';
 import Share from '../Share';
 import {
@@ -44,6 +44,7 @@ const Index: FC<IProps> = ({
   const toast = useToast();
   const navigate = useNavigate();
   const reportModal = useReportModal();
+  const dCaptcha = useCaptchaModal('delete');
 
   const refreshQuestion = () => {
     callback?.('default');
@@ -77,14 +78,28 @@ const Index: FC<IProps> = ({
         confirmBtnVariant: 'danger',
         confirmText: t('delete', { keyPrefix: 'btns' }),
         onConfirm: () => {
-          deleteQuestion({
-            id: qid,
-          }).then(() => {
-            toast.onShow({
-              msg: t('post_deleted', { keyPrefix: 'messages' }),
-              variant: 'success',
-            });
-            callback?.('delete_question');
+          dCaptcha.check(() => {
+            const req = {
+              id: qid,
+              captcha_code: undefined,
+              captcha_id: undefined,
+            };
+            dCaptcha.resolveCaptchaReq(req);
+
+            deleteQuestion(req)
+              .then(async () => {
+                await dCaptcha.close();
+                toast.onShow({
+                  msg: t('post_deleted', { keyPrefix: 'messages' }),
+                  variant: 'success',
+                });
+                callback?.('delete_question');
+              })
+              .catch((ex) => {
+                if (ex.isError) {
+                  dCaptcha.handleCaptchaError(ex.list);
+                }
+              });
           });
         },
       });
@@ -98,15 +113,29 @@ const Index: FC<IProps> = ({
         confirmBtnVariant: 'danger',
         confirmText: t('delete', { keyPrefix: 'btns' }),
         onConfirm: () => {
-          deleteAnswer({
-            id: aid,
-          }).then(() => {
-            // refresh page
-            toast.onShow({
-              msg: t('tip_answer_deleted'),
-              variant: 'success',
-            });
-            callback?.('all');
+          dCaptcha.check(() => {
+            const req = {
+              id: aid,
+              captcha_code: undefined,
+              captcha_id: undefined,
+            };
+            dCaptcha.resolveCaptchaReq(req);
+
+            deleteAnswer(req)
+              .then(async () => {
+                await dCaptcha.close();
+                // refresh page
+                toast.onShow({
+                  msg: t('tip_answer_deleted'),
+                  variant: 'success',
+                });
+                callback?.('all');
+              })
+              .catch((ex) => {
+                if (ex.isError) {
+                  dCaptcha.handleCaptchaError(ex.list);
+                }
+              });
           });
         },
       });
diff --git a/ui/src/components/Unactivate/index.tsx b/ui/src/components/Unactivate/index.tsx
index e45eea5d..f5673d7d 100644
--- a/ui/src/components/Unactivate/index.tsx
+++ b/ui/src/components/Unactivate/index.tsx
@@ -1,24 +1,21 @@
-import React, { useState, useEffect } from 'react';
+import React, { useState } from 'react';
 import { Button, Col } from 'react-bootstrap';
 import { Trans, useTranslation } from 'react-i18next';
 import { Link } from 'react-router-dom';
 
-import { PicAuthCodeModal } from '@/components/Modal';
-import type { ImgCodeRes, ImgCodeReq, FormDataType } from '@/common/interface';
+import type { ImgCodeReq, FormDataType } from '@/common/interface';
 import { loggedUserInfoStore } from '@/stores';
-import { resendEmail, checkImgCode } from '@/services';
-import { CAPTCHA_CODE_STORAGE_KEY } from '@/common/constants';
-import Storage from '@/utils/storage';
+import { resendEmail } from '@/services';
 import { handleFormError } from '@/utils';
+import { useCaptchaModal } from '@/hooks';
 
 interface IProps {
-  visible: boolean;
+  visible?: boolean;
 }
 
-const Index: React.FC<IProps> = ({ visible = false }) => {
+const Index: React.FC<IProps> = () => {
   const { t } = useTranslation('translation', { keyPrefix: 'inactive' });
   const [isSuccess, setSuccess] = useState(false);
-  const [showModal, setModalState] = useState(false);
   const { e_mail } = loggedUserInfoStore((state) => state.user);
   const [formData, setFormData] = useState<FormDataType>({
     captcha_code: {
@@ -27,75 +24,39 @@ const Index: React.FC<IProps> = ({ visible = false }) => {
       errorMsg: '',
     },
   });
-  const [imgCode, setImgCode] = useState<ImgCodeRes>({
-    captcha_id: '',
-    captcha_img: '',
-    verify: false,
-  });
 
-  const getImgCode = () => {
-    checkImgCode({
-      action: 'e_mail',
-    }).then((res) => {
-      setImgCode(res);
-    });
-  };
+  const emailCaptcha = useCaptchaModal('email');
 
-  const submit = (e?: any) => {
-    if (e) {
-      e.preventDefault();
-    }
-    let obj: ImgCodeReq = {};
+  const submit = () => {
+    let req: ImgCodeReq = {};
+    const imgCode = emailCaptcha.getCaptcha();
     if (imgCode.verify) {
-      const code = Storage.get(CAPTCHA_CODE_STORAGE_KEY) || '';
-      obj = {
-        captcha_code: code,
+      req = {
+        captcha_code: imgCode.captcha_code,
         captcha_id: imgCode.captcha_id,
       };
     }
-    resendEmail(obj)
+    resendEmail(req)
       .then(() => {
+        emailCaptcha.close();
         setSuccess(true);
-        setModalState(false);
       })
       .catch((err) => {
         if (err.isError) {
+          emailCaptcha.handleCaptchaError(err.list);
           const data = handleFormError(err, formData);
           setFormData({ ...data });
         }
-      })
-      .finally(() => {
-        getImgCode();
       });
   };
 
-  const onSentEmail = () => {
-    if (imgCode.verify) {
-      setModalState(true);
-      if (!formData.captcha_code.value) {
-        setFormData({
-          captcha_code: {
-            value: '',
-            isInvalid: false,
-            errorMsg: t('msg.empty'),
-          },
-        });
-      }
-      return;
-    }
-    submit();
+  const onSentEmail = (evt) => {
+    evt.preventDefault();
+    emailCaptcha.check(() => {
+      submit();
+    });
   };
 
-  const handleChange = (params: FormDataType) => {
-    setFormData({ ...formData, ...params });
-  };
-
-  useEffect(() => {
-    if (visible) {
-      getImgCode();
-    }
-  }, [visible]);
-
   return (
     <Col md={6} className="mx-auto text-center">
       {isSuccess ? (
@@ -124,18 +85,6 @@ const Index: React.FC<IProps> = ({ visible = false }) => {
           </Link>
         </>
       )}
-
-      <PicAuthCodeModal
-        visible={showModal}
-        data={{
-          captcha: formData.captcha_code,
-          imgCode,
-        }}
-        handleCaptcha={handleChange}
-        clickSubmit={submit}
-        refreshImgCode={getImgCode}
-        onClose={() => setModalState(false)}
-      />
     </Col>
   );
 };
diff --git a/ui/src/hooks/index.ts b/ui/src/hooks/index.ts
index 0538d339..0aa11a3b 100644
--- a/ui/src/hooks/index.ts
+++ b/ui/src/hooks/index.ts
@@ -11,6 +11,7 @@ import usePageTags from './usePageTags';
 import useLoginRedirect from './useLoginRedirect';
 import usePromptWithUnload from './usePrompt';
 import useActivationEmailModal from './useActivationEmailModal';
+import useCaptchaModal from './useCaptchaModal';
 
 export {
   useTagModal,
@@ -26,4 +27,5 @@ export {
   useLoginRedirect,
   usePromptWithUnload,
   useActivationEmailModal,
+  useCaptchaModal,
 };
diff --git a/ui/src/hooks/useCaptchaModal/index.tsx b/ui/src/hooks/useCaptchaModal/index.tsx
new file mode 100644
index 00000000..170f076e
--- /dev/null
+++ b/ui/src/hooks/useCaptchaModal/index.tsx
@@ -0,0 +1,253 @@
+import { useEffect, useRef, useState, useLayoutEffect } from 'react';
+import { Modal, Form, Button, InputGroup } from 'react-bootstrap';
+import { useTranslation } from 'react-i18next';
+
+import ReactDOM from 'react-dom/client';
+
+import { Icon } from '@/components';
+import type {
+  FormValue,
+  ImgCodeRes,
+  CaptchaKey,
+  FieldError,
+  ImgCodeReq,
+} from '@/common/interface';
+import { checkImgCode } from '@/services';
+
+type SubmitCallback = {
+  (): void;
+};
+
+const Index = (captchaKey: CaptchaKey) => {
+  const refRoot = useRef(null);
+  if (refRoot.current === null) {
+    // @ts-ignore
+    refRoot.current = ReactDOM.createRoot(document.createElement('div'));
+  }
+
+  const { t } = useTranslation('translation', { keyPrefix: 'pic_auth_code' });
+  const refKey = useRef<CaptchaKey>(captchaKey);
+  const refCallback = useRef<SubmitCallback>();
+  const pending = useRef(false);
+  const autoInitCaptchaData = /email/i.test(refKey.current);
+
+  const [stateShow, setStateShow] = useState(false);
+  const [captcha, setCaptcha] = useState<ImgCodeRes>({
+    captcha_id: '',
+    captcha_img: '',
+    verify: false,
+  });
+  const [imgCode, setImgCode] = useState<FormValue>({
+    value: '',
+    isInvalid: false,
+    errorMsg: '',
+  });
+  const refCaptcha = useRef(captcha);
+  const refImgCode = useRef(imgCode);
+
+  const fetchCaptchaData = () => {
+    pending.current = true;
+    checkImgCode(refKey.current)
+      .then((resp) => {
+        setCaptcha(resp);
+      })
+      .finally(() => {
+        pending.current = false;
+      });
+  };
+
+  const resetCapture = () => {
+    setCaptcha({
+      captcha_id: '',
+      captcha_img: '',
+      verify: false,
+    });
+  };
+
+  const show = () => {
+    if (!stateShow) {
+      setStateShow(true);
+    }
+  };
+  /**
+   * There are some cases where the React scheduler cancels the execution of some functions,
+   * which prevents them from closing properly:
+   *  for example, if the parent component uninstalls the child component directly,
+   *  and the `captchaModal.close()` call is inside the child component.
+   * In this case, call `await captchaModal.close()` and wait for the close action to complete.
+   */
+  const close = (reset = true) => {
+    setStateShow(false);
+    if (reset) {
+      resetCapture();
+    }
+    const p = new Promise<void>((resolve) => {
+      setTimeout(resolve);
+    });
+    return p;
+  };
+
+  const handleCaptchaError = (fel: FieldError[] = []) => {
+    const captchaErr = fel.find((o) => {
+      return o.error_field === 'captcha_code';
+    });
+
+    const ri = refImgCode.current;
+    if (captchaErr) {
+      /**
+       * `imgCode.value` No value but a validation error is received,
+       * indicating that it is the first time the interface has returned a CAPTCHA error,
+       * triggering the CAPTCHA logic. There is no need to display the error message at this point.
+       */
+      if (ri.value) {
+        setImgCode({
+          ...ri,
+          isInvalid: true,
+          errorMsg: captchaErr.error_msg,
+        });
+      }
+      fetchCaptchaData();
+    } else {
+      setImgCode({
+        ...ri,
+        isInvalid: false,
+        errorMsg: '',
+      });
+      close();
+    }
+  };
+
+  const handleChange = (evt) => {
+    evt.preventDefault();
+    setImgCode({
+      value: evt.target.value || '',
+      isInvalid: false,
+      errorMsg: '',
+    });
+  };
+
+  const getCaptcha = () => {
+    const rc = refCaptcha.current;
+    const ri = refImgCode.current;
+    const r = {
+      verify: !!rc?.verify,
+      captcha_id: rc?.captcha_id,
+      captcha_code: ri.value,
+    };
+
+    return r;
+  };
+
+  const resolveCaptchaReq = (req: ImgCodeReq) => {
+    const r = getCaptcha();
+    if (r.verify) {
+      req.captcha_code = r.captcha_code;
+      req.captcha_id = r.captcha_id;
+    }
+  };
+
+  const handleSubmit = (evt) => {
+    evt.preventDefault();
+    if (!imgCode.value) {
+      return;
+    }
+
+    if (refCallback.current) {
+      refCallback.current();
+    }
+  };
+
+  useEffect(() => {
+    if (autoInitCaptchaData) {
+      fetchCaptchaData();
+    }
+  }, []);
+
+  useLayoutEffect(() => {
+    refImgCode.current = imgCode;
+    refCaptcha.current = captcha;
+  }, [captcha, imgCode]);
+
+  useEffect(() => {
+    // @ts-ignore
+    refRoot.current.render(
+      <Modal
+        size="sm"
+        title="Captcha"
+        show={stateShow}
+        onHide={() => close(false)}
+        centered>
+        <Modal.Header closeButton>
+          <Modal.Title as="h5">{t('title')}</Modal.Title>
+        </Modal.Header>
+        <Modal.Body>
+          <Form noValidate onSubmit={handleSubmit}>
+            <Form.Group controlId="code" className="mb-3">
+              <div className="mb-3 p-2 d-flex align-items-center justify-content-center bg-light rounded-2">
+                <img
+                  src={captcha?.captcha_img}
+                  alt="captcha img"
+                  width="auto"
+                  height="40px"
+                />
+              </div>
+              <InputGroup>
+                <Form.Control
+                  type="text"
+                  autoComplete="off"
+                  placeholder={t('placeholder')}
+                  isInvalid={imgCode?.isInvalid}
+                  onChange={handleChange}
+                  value={imgCode.value}
+                />
+                <Button
+                  onClick={fetchCaptchaData}
+                  variant="outline-secondary"
+                  title={t('refresh', { keyPrefix: 'btns' })}
+                  style={{
+                    borderTopRightRadius: '0.375rem',
+                    borderBottomRightRadius: '0.375rem',
+                  }}>
+                  <Icon name="arrow-repeat" />
+                </Button>
+
+                <Form.Control.Feedback type="invalid">
+                  {imgCode?.errorMsg}
+                </Form.Control.Feedback>
+              </InputGroup>
+            </Form.Group>
+
+            <div className="d-grid">
+              <Button type="submit" disabled={!imgCode.value}>
+                {t('verify', { keyPrefix: 'btns' })}
+              </Button>
+            </div>
+          </Form>
+        </Modal.Body>
+      </Modal>,
+    );
+  });
+
+  const r = {
+    close,
+    show,
+    check: (submitFunc: SubmitCallback) => {
+      if (pending.current) {
+        return false;
+      }
+      refCallback.current = submitFunc;
+      if (captcha?.verify) {
+        show();
+      }
+      return submitFunc();
+    },
+    getCaptcha,
+    resolveCaptchaReq,
+    fetchCaptchaData,
+    handleCaptchaError,
+  };
+
+  return r;
+};
+
+export default Index;
diff --git a/ui/src/hooks/useReportModal/index.tsx b/ui/src/hooks/useReportModal/index.tsx
index 748564d9..d0d29e91 100644
--- a/ui/src/hooks/useReportModal/index.tsx
+++ b/ui/src/hooks/useReportModal/index.tsx
@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
 
 import ReactDOM from 'react-dom/client';
 
-import { useToast } from '@/hooks';
+import { useToast, useCaptchaModal } from '@/hooks';
 import type * as Type from '@/common/interface';
 import { reportList, postReport, closeQuestion, putReport } from '@/services';
 
@@ -37,6 +37,8 @@ const useReportModal = (callback?: () => void) => {
   const [show, setShow] = useState(false);
   const [list, setList] = useState<any[]>([]);
 
+  const rCaptcha = useCaptchaModal('report');
+
   useEffect(() => {
     const div = document.createElement('div');
     rootRef.current.root = ReactDOM.createRoot(div);
@@ -103,18 +105,32 @@ const useReportModal = (callback?: () => void) => {
       return;
     }
     if (!params.isBackend && params.action === 'flag') {
-      postReport({
-        source: params.type,
-        report_type: reportType.type,
-        object_id: params.id,
-        content: content.value,
-      }).then(() => {
-        toast.onShow({
-          msg: t('flag_success', { keyPrefix: 'toast' }),
-          variant: 'warning',
-        });
-        onClose();
-        asyncCallback();
+      rCaptcha.check(() => {
+        const flagReq = {
+          source: params.type,
+          report_type: reportType.type,
+          object_id: params.id,
+          content: content.value,
+          captcha_code: undefined,
+          captcha_id: undefined,
+        };
+        rCaptcha.resolveCaptchaReq(flagReq);
+
+        postReport(flagReq)
+          .then(async () => {
+            await rCaptcha.close();
+            toast.onShow({
+              msg: t('flag_success', { keyPrefix: 'toast' }),
+              variant: 'warning',
+            });
+            onClose();
+            asyncCallback();
+          })
+          .catch((ex) => {
+            if (ex.isError) {
+              rCaptcha.handleCaptchaError(ex.list);
+            }
+          });
       });
     }
 
diff --git a/ui/src/pages/Questions/Ask/index.tsx b/ui/src/pages/Questions/Ask/index.tsx
index 0f3b7f47..03b2cda6 100644
--- a/ui/src/pages/Questions/Ask/index.tsx
+++ b/ui/src/pages/Questions/Ask/index.tsx
@@ -7,7 +7,7 @@ import dayjs from 'dayjs';
 import classNames from 'classnames';
 import { isEqual } from 'lodash';
 
-import { usePageTags, usePromptWithUnload } from '@/hooks';
+import { usePageTags, usePromptWithUnload, useCaptchaModal } from '@/hooks';
 import { Editor, EditorRef, TagSelector } from '@/components';
 import type * as Type from '@/common/interface';
 import { DRAFT_QUESTION_STORAGE_KEY } from '@/common/constants';
@@ -102,6 +102,9 @@ const Ask = () => {
     isEdit ? '' : formData.title.value,
   );
 
+  const saveCaptcha = useCaptchaModal('question');
+  const editCaptcha = useCaptchaModal('edit');
+
   const removeDraft = () => {
     saveDraft.save.cancel();
     saveDraft.remove();
@@ -251,52 +254,69 @@ const Ask = () => {
       tags: formData.tags.value,
     };
     if (isEdit) {
-      modifyQuestion({
-        ...params,
-        id: qid,
-        edit_summary: formData.edit_summary.value,
-      })
-        .then((res) => {
-          navigate(pathFactory.questionLanding(qid, params.url_title), {
-            state: { isReview: res?.wait_for_review },
-          });
-        })
-        .catch((err) => {
-          if (err.isError) {
-            const data = handleFormError(err, formData);
-            setFormData({ ...data });
-          }
-        });
-    } else {
-      let res;
-      if (checked) {
-        res = await saveQuestionWidthAnaser({
+      editCaptcha.check(() => {
+        const ep = {
           ...params,
-          answer_content: formData.answer_content.value,
-        }).catch((err) => {
-          if (err.isError) {
-            const data = handleFormError(err, formData);
-            setFormData({ ...data });
-          }
-        });
-      } else {
-        res = await saveQuestion(params).catch((err) => {
-          if (err.isError) {
-            const data = handleFormError(err, formData);
-            setFormData({ ...data });
-          }
-        });
-      }
-
-      const id = res?.id || res?.question?.id;
-      if (id) {
-        if (checked) {
-          navigate(pathFactory.questionLanding(id, res?.question?.url_title));
-        } else {
-          navigate(pathFactory.questionLanding(id));
+          id: qid,
+          edit_summary: formData.edit_summary.value,
+        };
+        const imgCode = editCaptcha.getCaptcha();
+        if (imgCode.verify) {
+          ep.captcha_code = imgCode.captcha_code;
+          ep.captcha_id = imgCode.captcha_id;
         }
-      }
-      removeDraft();
+        modifyQuestion(ep)
+          .then(async (res) => {
+            await editCaptcha.close();
+            navigate(pathFactory.questionLanding(qid, params.url_title), {
+              state: { isReview: res?.wait_for_review },
+            });
+          })
+          .catch((err) => {
+            if (err.isError) {
+              editCaptcha.handleCaptchaError(err.list);
+              const data = handleFormError(err, formData);
+              setFormData({ ...data });
+            }
+          });
+      });
+    } else {
+      saveCaptcha.check(async () => {
+        const imgCode = saveCaptcha.getCaptcha();
+        if (imgCode.verify) {
+          params.captcha_code = imgCode.captcha_code;
+          params.captcha_id = imgCode.captcha_id;
+        }
+        let res;
+        if (checked) {
+          res = await saveQuestionWidthAnaser({
+            ...params,
+            answer_content: formData.answer_content.value,
+          }).catch((err) => {
+            if (err.isError) {
+              const data = handleFormError(err, formData);
+              setFormData({ ...data });
+            }
+          });
+        } else {
+          res = await saveQuestion(params).catch((err) => {
+            if (err.isError) {
+              const data = handleFormError(err, formData);
+              setFormData({ ...data });
+            }
+          });
+        }
+
+        const id = res?.id || res?.question?.id;
+        if (id) {
+          if (checked) {
+            navigate(pathFactory.questionLanding(id, res?.question?.url_title));
+          } else {
+            navigate(pathFactory.questionLanding(id));
+          }
+        }
+        removeDraft();
+      });
     }
   };
   const backPage = () => {
diff --git a/ui/src/pages/Questions/Detail/components/InviteToAnswer/index.tsx b/ui/src/pages/Questions/Detail/components/InviteToAnswer/index.tsx
index 7c8bf16e..b3c41631 100644
--- a/ui/src/pages/Questions/Detail/components/InviteToAnswer/index.tsx
+++ b/ui/src/pages/Questions/Detail/components/InviteToAnswer/index.tsx
@@ -8,6 +8,7 @@ import classNames from 'classnames';
 import { Avatar } from '@/components';
 import { getInviteUser, putInviteUser } from '@/services';
 import type * as Type from '@/common/interface';
+import { useCaptchaModal } from '@/hooks';
 
 import PeopleDropdown from './PeopleDropdown';
 
@@ -22,6 +23,7 @@ const Index: FC<Props> = ({ questionId, readOnly = false }) => {
   const MAX_ASK_NUMBER = 5;
   const [editing, setEditing] = useState(false);
   const [users, setUsers] = useState<Type.UserInfoBase[]>();
+  const iaCaptcha = useCaptchaModal('invitation_answer');
 
   const initInviteUsers = () => {
     if (!questionId) {
@@ -60,14 +62,23 @@ const Index: FC<Props> = ({ questionId, readOnly = false }) => {
     const names = users.map((_) => {
       return _.username;
     });
-    putInviteUser(questionId, names)
-      .then(() => {
-        setEditing(false);
-      })
-      .catch((ex) => {
-        console.log('ex: ', ex);
-      });
+    iaCaptcha.check(() => {
+      const imgCode: Type.ImgCodeReq = {};
+      iaCaptcha.resolveCaptchaReq(imgCode);
+      putInviteUser(questionId, names, imgCode)
+        .then(async () => {
+          await iaCaptcha.close();
+          setEditing(false);
+        })
+        .catch((ex) => {
+          if (ex.isError) {
+            iaCaptcha.handleCaptchaError(ex.list);
+          }
+          console.log('ex: ', ex);
+        });
+    });
   };
+
   useEffect(() => {
     initInviteUsers();
   }, [questionId]);
diff --git a/ui/src/pages/Questions/Detail/components/WriteAnswer/index.tsx b/ui/src/pages/Questions/Detail/components/WriteAnswer/index.tsx
index b80e767c..b1d84ec6 100644
--- a/ui/src/pages/Questions/Detail/components/WriteAnswer/index.tsx
+++ b/ui/src/pages/Questions/Detail/components/WriteAnswer/index.tsx
@@ -5,9 +5,9 @@ import { useTranslation, Trans } from 'react-i18next';
 import { marked } from 'marked';
 import classNames from 'classnames';
 
-import { usePromptWithUnload } from '@/hooks';
+import { usePromptWithUnload, useCaptchaModal } from '@/hooks';
 import { Editor, Modal, TextArea } from '@/components';
-import { FormDataType } from '@/common/interface';
+import { FormDataType, PostAnswerReq } from '@/common/interface';
 import { postAnswer } from '@/services';
 import { guard, handleFormError, SaveDraft, storageExpires } from '@/utils';
 import { DRAFT_ANSWER_STORAGE_KEY } from '@/common/constants';
@@ -41,6 +41,7 @@ const Index: FC<Props> = ({ visible = false, data, callback }) => {
   const [editorFocusState, setEditorFocusState] = useState(false);
   const [hasDraft, setHasDraft] = useState(false);
   const [showTips, setShowTips] = useState(data.loggedUserRank < 100);
+  const aCaptcha = useCaptchaModal('answer');
 
   usePromptWithUnload({
     when: Boolean(formData.content.value),
@@ -135,29 +136,40 @@ const Index: FC<Props> = ({ visible = false, data, callback }) => {
     if (!checkValidated()) {
       return;
     }
-    postAnswer({
-      question_id: data?.qid,
-      content: formData.content.value,
-      html: marked.parse(formData.content.value),
-    })
-      .then((res) => {
-        setShowEditor(false);
-        setFormData({
-          content: {
-            value: '',
-            isInvalid: false,
-            errorMsg: '',
-          },
+
+    aCaptcha.check(() => {
+      const params: PostAnswerReq = {
+        question_id: data?.qid,
+        content: formData.content.value,
+        html: marked.parse(formData.content.value),
+      };
+      const imgCode = aCaptcha.getCaptcha();
+      if (imgCode.verify) {
+        params.captcha_code = imgCode.captcha_code;
+        params.captcha_id = imgCode.captcha_id;
+      }
+      postAnswer(params)
+        .then(async (res) => {
+          await aCaptcha.close();
+          setShowEditor(false);
+          setFormData({
+            content: {
+              value: '',
+              isInvalid: false,
+              errorMsg: '',
+            },
+          });
+          removeDraft();
+          callback?.(res.info);
+        })
+        .catch((ex) => {
+          if (ex.isError) {
+            aCaptcha.handleCaptchaError(ex.list);
+            const stateData = handleFormError(ex, formData);
+            setFormData({ ...stateData });
+          }
         });
-        removeDraft();
-        callback?.(res.info);
-      })
-      .catch((ex) => {
-        if (ex.isError) {
-          const stateData = handleFormError(ex, formData);
-          setFormData({ ...stateData });
-        }
-      });
+    });
   };
 
   const clickBtn = () => {
diff --git a/ui/src/pages/Questions/EditAnswer/index.tsx b/ui/src/pages/Questions/EditAnswer/index.tsx
index c3b1342f..7f3e3ab2 100644
--- a/ui/src/pages/Questions/EditAnswer/index.tsx
+++ b/ui/src/pages/Questions/EditAnswer/index.tsx
@@ -7,7 +7,7 @@ import dayjs from 'dayjs';
 import classNames from 'classnames';
 
 import { handleFormError, scrollToDocTop } from '@/utils';
-import { usePageTags, usePromptWithUnload } from '@/hooks';
+import { usePageTags, usePromptWithUnload, useCaptchaModal } from '@/hooks';
 import { pathFactory } from '@/router/pathFactory';
 import { Editor, EditorRef, Icon, htmlRender } from '@/components';
 import type * as Type from '@/common/interface';
@@ -51,6 +51,7 @@ const Index = () => {
   const [formData, setFormData] = useState<FormDataItem>(initFormData);
   const [immData, setImmData] = useState(initFormData);
   const [contentChanged, setContentChanged] = useState(false);
+  const editCaptcha = useCaptchaModal('edit');
 
   useLayoutEffect(() => {
     if (data?.info?.content) {
@@ -136,36 +137,43 @@ const Index = () => {
 
     event.preventDefault();
     event.stopPropagation();
+
     if (!checkValidated()) {
       return;
     }
 
-    const params: Type.AnswerParams = {
-      content: formData.content.value,
-      html: editorRef.current.getHtml(),
-      question_id: qid,
-      id: aid,
-      edit_summary: formData.description.value,
-    };
-    modifyAnswer(params)
-      .then((res) => {
-        navigate(
-          pathFactory.answerLanding({
-            questionId: qid,
-            slugTitle: data?.question?.url_title,
-            answerId: aid,
-          }),
-          {
-            state: { isReview: res?.wait_for_review },
-          },
-        );
-      })
-      .catch((ex) => {
-        if (ex.isError) {
-          const stateData = handleFormError(ex, formData);
-          setFormData({ ...stateData });
-        }
-      });
+    editCaptcha.check(() => {
+      const params: Type.AnswerParams = {
+        content: formData.content.value,
+        html: editorRef.current.getHtml(),
+        question_id: qid,
+        id: aid,
+        edit_summary: formData.description.value,
+      };
+      editCaptcha.resolveCaptchaReq(params);
+
+      modifyAnswer(params)
+        .then(async (res) => {
+          await editCaptcha.close();
+          navigate(
+            pathFactory.answerLanding({
+              questionId: qid,
+              slugTitle: data?.question?.url_title,
+              answerId: aid,
+            }),
+            {
+              state: { isReview: res?.wait_for_review },
+            },
+          );
+        })
+        .catch((ex) => {
+          if (ex.isError) {
+            editCaptcha.handleCaptchaError(ex.list);
+            const stateData = handleFormError(ex, formData);
+            setFormData({ ...stateData });
+          }
+        });
+    });
   };
   const handleSelectedRevision = (e) => {
     const index = e.target.value;
diff --git a/ui/src/pages/Search/index.tsx b/ui/src/pages/Search/index.tsx
index 4ea10ee3..7443d05a 100644
--- a/ui/src/pages/Search/index.tsx
+++ b/ui/src/pages/Search/index.tsx
@@ -1,10 +1,12 @@
 import { Row, Col, ListGroup } from 'react-bootstrap';
 import { useTranslation } from 'react-i18next';
 import { useSearchParams } from 'react-router-dom';
+import { useEffect, useState } from 'react';
 
-import { usePageTags } from '@/hooks';
+import { usePageTags, useCaptchaModal } from '@/hooks';
 import { Pagination } from '@/components';
-import { useSearch } from '@/services';
+import { getSearchResult } from '@/services';
+import type { SearchParams, SearchRes } from '@/common/interface';
 
 import {
   Head,
@@ -21,15 +23,52 @@ const Index = () => {
   const page = searchParams.get('page') || 1;
   const q = searchParams.get('q') || '';
   const order = searchParams.get('order') || 'active';
-
-  const { data, isLoading } = useSearch({
-    q,
-    order,
-    page: Number(page),
-    size: 20,
+  const [isLoading, setIsLoading] = useState(false);
+  const [data, setData] = useState<SearchRes>({
+    count: 0,
+    list: [],
+    extra: null,
   });
-
   const { count = 0, list = [], extra = null } = data || {};
+
+  const searchCaptcha = useCaptchaModal('search');
+
+  const doSearch = () => {
+    setIsLoading(true);
+    const params: SearchParams = {
+      q,
+      order,
+      page: Number(page),
+      size: 20,
+    };
+
+    const captcha = searchCaptcha.getCaptcha();
+    if (captcha?.verify) {
+      params.captcha_id = captcha.captcha_id;
+      params.captcha_code = captcha.captcha_code;
+    }
+
+    getSearchResult(params)
+      .then((resp) => {
+        searchCaptcha.close();
+        setData(resp);
+      })
+      .catch((err) => {
+        if (err.isError) {
+          searchCaptcha.handleCaptchaError(err.list);
+        }
+      })
+      .finally(() => {
+        setIsLoading(false);
+      });
+  };
+
+  useEffect(() => {
+    searchCaptcha.check(() => {
+      doSearch();
+    });
+  }, [q, order, page]);
+
   let pageTitle = t('search', { keyPrefix: 'page_title' });
   if (q) {
     pageTitle = `${t('posts_containing', { keyPrefix: 'page_title' })} '${q}'`;
@@ -37,6 +76,7 @@ const Index = () => {
   usePageTags({
     title: pageTitle,
   });
+
   return (
     <Row className="pt-4 mb-5">
       <Col className="page-main flex-auto">
diff --git a/ui/src/pages/Users/AccountForgot/components/sendEmail.tsx b/ui/src/pages/Users/AccountForgot/components/sendEmail.tsx
index 8c6aa161..323d9113 100644
--- a/ui/src/pages/Users/AccountForgot/components/sendEmail.tsx
+++ b/ui/src/pages/Users/AccountForgot/components/sendEmail.tsx
@@ -1,22 +1,19 @@
-import { FC, memo, useEffect, useState } from 'react';
+import { FC, memo, useState } from 'react';
 import { Form, Button } from 'react-bootstrap';
 import { useTranslation } from 'react-i18next';
 
-import type {
-  ImgCodeRes,
-  PasswordResetReq,
-  FormDataType,
-} from '@/common/interface';
-import { resetPassword, checkImgCode } from '@/services';
-import { PicAuthCodeModal } from '@/components/Modal';
+import type { PasswordResetReq, FormDataType } from '@/common/interface';
+import { resetPassword } from '@/services';
 import { handleFormError } from '@/utils';
+import { useCaptchaModal } from '@/hooks';
 
 interface IProps {
-  visible: boolean;
+  // eslint-disable-next-line react/no-unused-prop-types
+  visible?: boolean;
   callback: (param: number, email: string) => void;
 }
 
-const Index: FC<IProps> = ({ visible = false, callback }) => {
+const Index: FC<IProps> = ({ callback }) => {
   const { t } = useTranslation('translation', { keyPrefix: 'account_forgot' });
   const [formData, setFormData] = useState<FormDataType>({
     e_mail: {
@@ -24,26 +21,9 @@ const Index: FC<IProps> = ({ visible = false, callback }) => {
       isInvalid: false,
       errorMsg: '',
     },
-    captcha_code: {
-      value: '',
-      isInvalid: false,
-      errorMsg: '',
-    },
   });
-  const [imgCode, setImgCode] = useState<ImgCodeRes>({
-    captcha_id: '',
-    captcha_img: '',
-    verify: false,
-  });
-  const [showModal, setModalState] = useState(false);
 
-  const getImgCode = () => {
-    checkImgCode({
-      action: 'find_pass',
-    }).then((res) => {
-      setImgCode(res);
-    });
-  };
+  const emailCaptcha = useCaptchaModal('email');
 
   const handleChange = (params: FormDataType) => {
     setFormData({ ...formData, ...params });
@@ -73,27 +53,24 @@ const Index: FC<IProps> = ({ visible = false, callback }) => {
     const params: PasswordResetReq = {
       e_mail: formData.e_mail.value,
     };
-    if (imgCode.verify) {
-      params.captcha_code = formData.captcha_code.value;
-      params.captcha_id = imgCode.captcha_id;
+
+    const captcha = emailCaptcha.getCaptcha();
+    if (captcha.verify) {
+      params.captcha_code = captcha.captcha_code;
+      params.captcha_id = captcha.captcha_id;
     }
 
     resetPassword(params)
-      .then(() => {
+      .then(async () => {
+        await emailCaptcha.close();
         callback?.(2, formData.e_mail.value);
-        setModalState(false);
       })
       .catch((err) => {
         if (err.isError) {
+          emailCaptcha.handleCaptchaError(err.list);
           const data = handleFormError(err, formData);
-          if (!err.list.find((v) => v.error_field.indexOf('captcha') >= 0)) {
-            setModalState(false);
-          }
           setFormData({ ...data });
         }
-      })
-      .finally(() => {
-        getImgCode();
       });
   };
 
@@ -105,64 +82,41 @@ const Index: FC<IProps> = ({ visible = false, callback }) => {
       return;
     }
 
-    if (imgCode.verify) {
-      setModalState(true);
-      return;
-    }
-
-    sendEmail();
+    emailCaptcha.check(() => {
+      sendEmail();
+    });
   };
 
-  useEffect(() => {
-    if (visible) {
-      getImgCode();
-    }
-  }, [visible]);
-
   return (
-    <>
-      <Form noValidate onSubmit={handleSubmit} autoComplete="off">
-        <Form.Group controlId="email" className="mb-3">
-          <Form.Label>{t('email.label')}</Form.Label>
-          <Form.Control
-            required
-            type="email"
-            value={formData.e_mail.value}
-            isInvalid={formData.e_mail.isInvalid}
-            onChange={(e) => {
-              handleChange({
-                e_mail: {
-                  value: e.target.value,
-                  isInvalid: false,
-                  errorMsg: '',
-                },
-              });
-            }}
-          />
-          <Form.Control.Feedback type="invalid">
-            {formData.e_mail.errorMsg}
-          </Form.Control.Feedback>
-        </Form.Group>
+    <Form noValidate onSubmit={handleSubmit} autoComplete="off">
+      <Form.Group controlId="email" className="mb-3">
+        <Form.Label>{t('email.label')}</Form.Label>
+        <Form.Control
+          required
+          type="email"
+          value={formData.e_mail.value}
+          isInvalid={formData.e_mail.isInvalid}
+          onChange={(e) => {
+            handleChange({
+              e_mail: {
+                value: e.target.value,
+                isInvalid: false,
+                errorMsg: '',
+              },
+            });
+          }}
+        />
+        <Form.Control.Feedback type="invalid">
+          {formData.e_mail.errorMsg}
+        </Form.Control.Feedback>
+      </Form.Group>
 
-        <div className="d-grid mb-3">
-          <Button variant="primary" type="submit">
-            {t('btn_name')}
-          </Button>
-        </div>
-      </Form>
-
-      <PicAuthCodeModal
-        visible={showModal}
-        data={{
-          captcha: formData.captcha_code,
-          imgCode,
-        }}
-        handleCaptcha={handleChange}
-        clickSubmit={sendEmail}
-        refreshImgCode={getImgCode}
-        onClose={() => setModalState(false)}
-      />
-    </>
+      <div className="d-grid mb-3">
+        <Button variant="primary" type="submit">
+          {t('btn_name')}
+        </Button>
+      </div>
+    </Form>
   );
 };
 
diff --git a/ui/src/pages/Users/ChangeEmail/components/sendEmail.tsx b/ui/src/pages/Users/ChangeEmail/components/sendEmail.tsx
index 6f884ca0..7143c099 100644
--- a/ui/src/pages/Users/ChangeEmail/components/sendEmail.tsx
+++ b/ui/src/pages/Users/ChangeEmail/components/sendEmail.tsx
@@ -1,17 +1,13 @@
-import { FC, memo, useEffect, useState } from 'react';
+import { FC, memo, useState } from 'react';
 import { Form, Button } from 'react-bootstrap';
 import { useTranslation } from 'react-i18next';
 import { useNavigate } from 'react-router-dom';
 
-import type {
-  ImgCodeRes,
-  PasswordResetReq,
-  FormDataType,
-} from '@/common/interface';
+import type { PasswordResetReq, FormDataType } from '@/common/interface';
 import { loggedUserInfoStore } from '@/stores';
-import { changeEmail, checkImgCode } from '@/services';
-import { PicAuthCodeModal } from '@/components/Modal';
+import { changeEmail } from '@/services';
 import { handleFormError } from '@/utils';
+import { useCaptchaModal } from '@/hooks';
 
 const Index: FC = () => {
   const { t } = useTranslation('translation', { keyPrefix: 'change_email' });
@@ -21,28 +17,12 @@ const Index: FC = () => {
       isInvalid: false,
       errorMsg: '',
     },
-    captcha_code: {
-      value: '',
-      isInvalid: false,
-      errorMsg: '',
-    },
   });
-  const [imgCode, setImgCode] = useState<ImgCodeRes>({
-    captcha_id: '',
-    captcha_img: '',
-    verify: false,
-  });
-  const [showModal, setModalState] = useState(false);
+
   const navigate = useNavigate();
   const { user: userInfo, update: updateUser } = loggedUserInfoStore();
 
-  const getImgCode = () => {
-    checkImgCode({
-      action: 'e_mail',
-    }).then((res) => {
-      setImgCode(res);
-    });
-  };
+  const emailCaptcha = useCaptchaModal('email');
 
   const handleChange = (params: FormDataType) => {
     setFormData({ ...formData, ...params });
@@ -72,28 +52,25 @@ const Index: FC = () => {
     const params: PasswordResetReq = {
       e_mail: formData.e_mail.value,
     };
+    const imgCode = emailCaptcha.getCaptcha();
     if (imgCode.verify) {
-      params.captcha_code = formData.captcha_code.value;
+      params.captcha_code = imgCode.captcha_code;
       params.captcha_id = imgCode.captcha_id;
     }
+
     changeEmail(params)
-      .then(() => {
+      .then(async () => {
+        await emailCaptcha.close();
         userInfo.e_mail = formData.e_mail.value;
         updateUser(userInfo);
         navigate('/users/login', { replace: true });
-        setModalState(false);
       })
       .catch((err) => {
         if (err.isError) {
+          emailCaptcha.handleCaptchaError(err.list);
           const data = handleFormError(err, formData);
-          if (!err.list.find((v) => v.error_field.indexOf('captcha') >= 0)) {
-            setModalState(false);
-          }
           setFormData({ ...data });
         }
-      })
-      .finally(() => {
-        getImgCode();
       });
   };
 
@@ -104,69 +81,48 @@ const Index: FC = () => {
       return;
     }
 
-    if (imgCode.verify) {
-      setModalState(true);
-      return;
-    }
-
-    sendEmail();
+    emailCaptcha.check(() => {
+      sendEmail();
+    });
   };
 
   const goBack = () => {
     navigate('/users/login?status=inactive', { replace: true });
   };
 
-  useEffect(() => {
-    getImgCode();
-  }, []);
-
   return (
-    <>
-      <Form noValidate onSubmit={handleSubmit} autoComplete="off">
-        <Form.Group controlId="email" className="mb-3">
-          <Form.Label>{t('email.label')}</Form.Label>
-          <Form.Control
-            required
-            type="email"
-            value={formData.e_mail.value}
-            isInvalid={formData.e_mail.isInvalid}
-            onChange={(e) => {
-              handleChange({
-                e_mail: {
-                  value: e.target.value,
-                  isInvalid: false,
-                  errorMsg: '',
-                },
-              });
-            }}
-          />
-          <Form.Control.Feedback type="invalid">
-            {formData.e_mail.errorMsg}
-          </Form.Control.Feedback>
-        </Form.Group>
+    <Form noValidate onSubmit={handleSubmit} autoComplete="off">
+      <Form.Group controlId="email" className="mb-3">
+        <Form.Label>{t('email.label')}</Form.Label>
+        <Form.Control
+          required
+          type="email"
+          value={formData.e_mail.value}
+          isInvalid={formData.e_mail.isInvalid}
+          onChange={(e) => {
+            handleChange({
+              e_mail: {
+                value: e.target.value,
+                isInvalid: false,
+                errorMsg: '',
+              },
+            });
+          }}
+        />
+        <Form.Control.Feedback type="invalid">
+          {formData.e_mail.errorMsg}
+        </Form.Control.Feedback>
+      </Form.Group>
 
-        <div className="d-grid mb-3">
-          <Button variant="primary" type="submit">
-            {t('btn_update')}
-          </Button>
-          <Button variant="link" className="mt-2 d-block" onClick={goBack}>
-            {t('btn_cancel')}
-          </Button>
-        </div>
-      </Form>
-
-      <PicAuthCodeModal
-        visible={showModal}
-        data={{
-          captcha: formData.captcha_code,
-          imgCode,
-        }}
-        handleCaptcha={handleChange}
-        clickSubmit={sendEmail}
-        refreshImgCode={getImgCode}
-        onClose={() => setModalState(false)}
-      />
-    </>
+      <div className="d-grid mb-3">
+        <Button variant="primary" type="submit">
+          {t('btn_update')}
+        </Button>
+        <Button variant="link" className="mt-2 d-block" onClick={goBack}>
+          {t('btn_cancel')}
+        </Button>
+      </div>
+    </Form>
   );
 };
 
diff --git a/ui/src/pages/Users/Login/index.tsx b/ui/src/pages/Users/Login/index.tsx
index a6bedd59..56bcec78 100644
--- a/ui/src/pages/Users/Login/index.tsx
+++ b/ui/src/pages/Users/Login/index.tsx
@@ -3,12 +3,8 @@ import { Container, Form, Button, Col } from 'react-bootstrap';
 import { Link, useNavigate, useSearchParams } from 'react-router-dom';
 import { Trans, useTranslation } from 'react-i18next';
 
-import { usePageTags } from '@/hooks';
-import type {
-  LoginReqParams,
-  ImgCodeRes,
-  FormDataType,
-} from '@/common/interface';
+import { usePageTags, useCaptchaModal } from '@/hooks';
+import type { LoginReqParams, FormDataType } from '@/common/interface';
 import { Unactivate, WelcomeTitle, PluginRender } from '@/components';
 import {
   loggedUserInfoStore,
@@ -16,14 +12,12 @@ import {
   userCenterStore,
 } from '@/stores';
 import { floppyNavigation, guard, handleFormError, userCenter } from '@/utils';
-import { login, checkImgCode, UcAgent } from '@/services';
-import { PicAuthCodeModal } from '@/components/Modal';
+import { login, UcAgent } from '@/services';
 
 const Index: React.FC = () => {
   const { t } = useTranslation('translation', { keyPrefix: 'login' });
   const navigate = useNavigate();
   const [searchParams] = useSearchParams();
-  const [refresh, setRefresh] = useState(0);
   const { user: storeUser, update: updateUser } = loggedUserInfoStore((_) => _);
   const loginSetting = loginSettingStore((state) => state.login);
   const ucAgent = userCenterStore().agent;
@@ -45,34 +39,15 @@ const Index: React.FC = () => {
       isInvalid: false,
       errorMsg: '',
     },
-    captcha_code: {
-      value: '',
-      isInvalid: false,
-      errorMsg: '',
-    },
   });
-  const [imgCode, setImgCode] = useState<ImgCodeRes>({
-    captcha_id: '',
-    captcha_img: '',
-    verify: false,
-  });
-  const [showModal, setModalState] = useState(false);
+
   const [step, setStep] = useState(1);
 
   const handleChange = (params: FormDataType) => {
     setFormData({ ...formData, ...params });
   };
 
-  const getImgCode = () => {
-    if (!canOriginalLogin) {
-      return;
-    }
-    checkImgCode({
-      action: 'login',
-    }).then((res) => {
-      setImgCode(res);
-    });
-  };
+  const passwordCaptcha = useCaptchaModal('password');
 
   const checkValidated = (): boolean => {
     let bol = true;
@@ -110,34 +85,31 @@ const Index: React.FC = () => {
       e_mail: formData.e_mail.value,
       pass: formData.pass.value,
     };
-    if (imgCode.verify) {
-      params.captcha_code = formData.captcha_code.value;
-      params.captcha_id = imgCode.captcha_id;
+
+    const captcha = passwordCaptcha.getCaptcha();
+    if (captcha?.verify) {
+      params.captcha_code = captcha.captcha_code;
+      params.captcha_id = captcha.captcha_id;
     }
 
     login(params)
       .then((res) => {
+        passwordCaptcha.close();
         updateUser(res);
         const userStat = guard.deriveLoginState();
         if (userStat.isNotActivated) {
           // inactive
           setStep(2);
-          setRefresh((pre) => pre + 1);
         } else {
           guard.handleLoginRedirect(navigate);
         }
-
-        setModalState(false);
       })
       .catch((err) => {
         if (err.isError) {
           const data = handleFormError(err, formData);
-          if (!err.list.find((v) => v.error_field.indexOf('captcha') >= 0)) {
-            setModalState(false);
-          }
           setFormData({ ...data });
+          passwordCaptcha.handleCaptchaError(err.list);
         }
-        setRefresh((pre) => pre + 1);
       });
   };
 
@@ -149,18 +121,11 @@ const Index: React.FC = () => {
       return;
     }
 
-    if (imgCode.verify) {
-      setModalState(true);
-      return;
-    }
-
-    handleLogin();
+    passwordCaptcha.check(() => {
+      handleLogin();
+    });
   };
 
-  useEffect(() => {
-    getImgCode();
-  }, [refresh]);
-
   useEffect(() => {
     const isInactive = searchParams.get('status');
 
@@ -168,6 +133,7 @@ const Index: React.FC = () => {
       setStep(2);
     }
   }, []);
+
   usePageTags({
     title: t('login', { keyPrefix: 'page_title' }),
   });
@@ -263,18 +229,6 @@ const Index: React.FC = () => {
       ) : null}
 
       {step === 2 && <Unactivate visible={step === 2} />}
-
-      <PicAuthCodeModal
-        visible={showModal}
-        data={{
-          captcha: formData.captcha_code,
-          imgCode,
-        }}
-        handleCaptcha={handleChange}
-        clickSubmit={handleLogin}
-        refreshImgCode={getImgCode}
-        onClose={() => setModalState(false)}
-      />
     </Container>
   );
 };
diff --git a/ui/src/pages/Users/Register/components/SignUpForm/index.tsx b/ui/src/pages/Users/Register/components/SignUpForm/index.tsx
index 59474088..47768934 100644
--- a/ui/src/pages/Users/Register/components/SignUpForm/index.tsx
+++ b/ui/src/pages/Users/Register/components/SignUpForm/index.tsx
@@ -1,17 +1,11 @@
-import React, { FormEvent, MouseEvent, useEffect, useState } from 'react';
+import React, { FormEvent, MouseEvent, useState } from 'react';
 import { Form, Button } from 'react-bootstrap';
 import { Link } from 'react-router-dom';
 import { Trans, useTranslation } from 'react-i18next';
 
-import { PicAuthCodeModal } from '@/components/Modal';
-import { ImgCodeRes } from '@/common/interface';
+import { useCaptchaModal } from '@/hooks';
 import type { FormDataType, RegisterReqParams } from '@/common/interface';
-import {
-  register,
-  getRegisterCaptcha,
-  useLegalTos,
-  useLegalPrivacy,
-} from '@/services';
+import { register, useLegalTos, useLegalPrivacy } from '@/services';
 import userStore from '@/stores/loggedUserInfo';
 import { handleFormError } from '@/utils';
 
@@ -37,25 +31,11 @@ const Index: React.FC<Props> = ({ callback }) => {
       isInvalid: false,
       errorMsg: '',
     },
-    captcha_code: {
-      value: '',
-      isInvalid: false,
-      errorMsg: '',
-    },
   });
-  const updateUser = userStore((state) => state.update);
 
-  const [imgCode, setImgCode] = useState<ImgCodeRes>({
-    captcha_id: '',
-    captcha_img: '',
-    verify: false,
-  });
-  const [showModal, setModalState] = useState(false);
-  const getImgCode = () => {
-    getRegisterCaptcha().then((res) => {
-      setImgCode(res);
-    });
-  };
+  const updateUser = userStore((state) => state.update);
+  const emailCaptcha = useCaptchaModal('email');
+
   const handleChange = (params: FormDataType) => {
     setFormData({ ...formData, ...params });
   };
@@ -86,6 +66,7 @@ const Index: React.FC<Props> = ({ callback }) => {
     });
     return bol;
   };
+
   const { data: tos } = useLegalTos();
   const { data: privacy } = useLegalPrivacy();
   const argumentClick = (evt: MouseEvent, type: 'tos' | 'privacy') => {
@@ -117,25 +98,24 @@ const Index: React.FC<Props> = ({ callback }) => {
       pass: formData.pass.value,
     };
 
-    if (imgCode.verify) {
-      reqParams.captcha_code = formData.captcha_code.value;
-      reqParams.captcha_id = imgCode.captcha_id;
+    const captcha = emailCaptcha.getCaptcha();
+    if (captcha?.verify) {
+      reqParams.captcha_code = captcha.captcha_code;
+      reqParams.captcha_id = captcha.captcha_id;
     }
+
     register(reqParams)
       .then((res) => {
+        emailCaptcha.close();
         updateUser(res);
-        setModalState(false);
         callback();
       })
       .catch((err) => {
         if (err.isError) {
+          emailCaptcha.handleCaptchaError(err.list);
           const data = handleFormError(err, formData);
-          if (!err.list.find((v) => v.error_field.indexOf('captcha') >= 0)) {
-            setModalState(false);
-          }
           setFormData({ ...data });
         }
-        getImgCode();
       });
   };
 
@@ -145,15 +125,11 @@ const Index: React.FC<Props> = ({ callback }) => {
     if (!checkValidated()) {
       return;
     }
-    if (imgCode.verify) {
-      setModalState(true);
-      return;
-    }
-    handleRegister();
+    emailCaptcha.check(() => {
+      handleRegister();
+    });
   };
-  useEffect(() => {
-    getImgCode();
-  }, []);
+
   return (
     <>
       <Form noValidate onSubmit={handleSubmit} autoComplete="off">
@@ -260,18 +236,6 @@ const Index: React.FC<Props> = ({ callback }) => {
           Already have an account? <Link to="/users/login">Log in</Link>
         </Trans>
       </div>
-
-      <PicAuthCodeModal
-        visible={showModal}
-        data={{
-          captcha: formData.captcha_code,
-          imgCode,
-        }}
-        handleCaptcha={handleChange}
-        clickSubmit={handleRegister}
-        refreshImgCode={getImgCode}
-        onClose={() => setModalState(false)}
-      />
     </>
   );
 };
diff --git a/ui/src/pages/Users/Settings/Account/components/ModifyEmail/index.tsx b/ui/src/pages/Users/Settings/Account/components/ModifyEmail/index.tsx
index a934383d..3c5fdee1 100644
--- a/ui/src/pages/Users/Settings/Account/components/ModifyEmail/index.tsx
+++ b/ui/src/pages/Users/Settings/Account/components/ModifyEmail/index.tsx
@@ -3,22 +3,15 @@ import { Form, Button } from 'react-bootstrap';
 import { useTranslation } from 'react-i18next';
 
 import type * as Type from '@/common/interface';
-import { useToast } from '@/hooks';
-import { getLoggedUserInfo, changeEmail, checkImgCode } from '@/services';
+import { useToast, useCaptchaModal } from '@/hooks';
+import { getLoggedUserInfo, changeEmail } from '@/services';
 import { handleFormError } from '@/utils';
-import { PicAuthCodeModal } from '@/components';
 
 const Index: FC = () => {
   const { t } = useTranslation('translation', {
     keyPrefix: 'settings.account',
   });
   const [step, setStep] = useState(1);
-  const [showModal, setModalState] = useState(false);
-  const [imgCode, setImgCode] = useState<Type.ImgCodeRes>({
-    captcha_id: '',
-    captcha_img: '',
-    verify: false,
-  });
   const [formData, setFormData] = useState<Type.FormDataType>({
     e_mail: {
       value: '',
@@ -30,28 +23,17 @@ const Index: FC = () => {
       isInvalid: false,
       errorMsg: '',
     },
-    captcha_code: {
-      value: '',
-      isInvalid: false,
-      errorMsg: '',
-    },
   });
   const [userInfo, setUserInfo] = useState<Type.UserInfoRes>();
   const toast = useToast();
+  const emailCaptcha = useCaptchaModal('edit_userinfo');
+
   useEffect(() => {
     getLoggedUserInfo().then((resp) => {
       setUserInfo(resp);
     });
   }, []);
 
-  const getImgCode = () => {
-    checkImgCode({
-      action: 'e_mail',
-    }).then((res) => {
-      setImgCode(res);
-    });
-  };
-
   const handleChange = (params: Type.FormDataType) => {
     setFormData({ ...formData, ...params });
   };
@@ -95,11 +77,6 @@ const Index: FC = () => {
         isInvalid: false,
         errorMsg: '',
       },
-      captcha_code: {
-        value: '',
-        isInvalid: false,
-        errorMsg: '',
-      },
     });
   };
 
@@ -112,14 +89,15 @@ const Index: FC = () => {
       pass: formData.pass.value,
     };
 
+    const imgCode = emailCaptcha.getCaptcha();
     if (imgCode.verify) {
-      params.captcha_code = formData.captcha_code.value;
+      params.captcha_code = imgCode.captcha_code;
       params.captcha_id = imgCode.captcha_id;
     }
     changeEmail(params)
-      .then(() => {
+      .then(async () => {
+        await emailCaptcha.close();
         setStep(1);
-        setModalState(false);
         toast.onShow({
           msg: t('change_email_info'),
           variant: 'warning',
@@ -128,15 +106,10 @@ const Index: FC = () => {
       })
       .catch((err) => {
         if (err.isError) {
+          emailCaptcha.handleCaptchaError(err.list);
           const data = handleFormError(err, formData);
           setFormData({ ...data });
-          if (!err.list.find((v) => v.error_field.indexOf('captcha') >= 0)) {
-            setModalState(false);
-          }
         }
-      })
-      .finally(() => {
-        getImgCode();
       });
   };
 
@@ -147,11 +120,9 @@ const Index: FC = () => {
       return;
     }
 
-    if (imgCode.verify) {
-      setModalState(true);
-      return;
-    }
-    postEmail();
+    emailCaptcha.check(() => {
+      postEmail();
+    });
   };
 
   return (
@@ -174,7 +145,6 @@ const Index: FC = () => {
             variant="outline-secondary"
             onClick={() => {
               setStep(2);
-              getImgCode();
             }}>
             {t('change_email_btn')}
           </Button>
@@ -240,18 +210,6 @@ const Index: FC = () => {
           </div>
         </Form>
       )}
-
-      <PicAuthCodeModal
-        visible={showModal}
-        data={{
-          captcha: formData.captcha_code,
-          imgCode,
-        }}
-        handleCaptcha={handleChange}
-        clickSubmit={postEmail}
-        refreshImgCode={getImgCode}
-        onClose={() => setModalState(false)}
-      />
     </div>
   );
 };
diff --git a/ui/src/pages/Users/Settings/Account/components/ModifyPass/index.tsx b/ui/src/pages/Users/Settings/Account/components/ModifyPass/index.tsx
index a06cb987..3caf7c76 100644
--- a/ui/src/pages/Users/Settings/Account/components/ModifyPass/index.tsx
+++ b/ui/src/pages/Users/Settings/Account/components/ModifyPass/index.tsx
@@ -4,12 +4,11 @@ import { useTranslation } from 'react-i18next';
 
 import classname from 'classnames';
 
-import { useToast } from '@/hooks';
-import type { FormDataType, ImgCodeRes } from '@/common/interface';
-import { modifyPassword, checkImgCode } from '@/services';
+import { useToast, useCaptchaModal } from '@/hooks';
+import type { FormDataType } from '@/common/interface';
+import { modifyPassword } from '@/services';
 import { handleFormError } from '@/utils';
 import { loggedUserInfoStore } from '@/stores';
-import { PicAuthCodeModal } from '@/components';
 
 const Index: FC = () => {
   const { t } = useTranslation('translation', {
@@ -35,20 +34,8 @@ const Index: FC = () => {
       errorMsg: '',
     },
   });
-  const [showModal, setModalState] = useState(false);
-  const [imgCode, setImgCode] = useState<ImgCodeRes>({
-    captcha_id: '',
-    captcha_img: '',
-    verify: false,
-  });
 
-  const getImgCode = () => {
-    checkImgCode({
-      action: 'modify_pass',
-    }).then((res) => {
-      setImgCode(res);
-    });
-  };
+  const infoCaptcha = useCaptchaModal('edit_userinfo');
 
   const handleFormState = () => {
     setFormState((pre) => !pre);
@@ -128,13 +115,14 @@ const Index: FC = () => {
       pass: formData.pass.value,
     };
 
+    const imgCode = infoCaptcha.getCaptcha();
     if (imgCode.verify) {
-      params.captcha_code = formData.captcha_code.value;
+      params.captcha_code = imgCode.captcha_code;
       params.captcha_id = imgCode.captcha_id;
     }
     modifyPassword(params)
-      .then(() => {
-        setModalState(false);
+      .then(async () => {
+        await infoCaptcha.close();
         toast.onShow({
           msg: t('update_password', { keyPrefix: 'toast' }),
           variant: 'success',
@@ -143,15 +131,10 @@ const Index: FC = () => {
       })
       .catch((err) => {
         if (err.isError) {
+          infoCaptcha.handleCaptchaError(err.list);
           const data = handleFormError(err, formData);
-          if (!err.list.find((v) => v.error_field.indexOf('captcha') >= 0)) {
-            setModalState(false);
-          }
           setFormData({ ...data });
         }
-      })
-      .finally(() => {
-        getImgCode();
       });
   };
 
@@ -162,11 +145,9 @@ const Index: FC = () => {
       return;
     }
 
-    if (imgCode.verify) {
-      setModalState(true);
-      return;
-    }
-    postModifyPass();
+    infoCaptcha.check(() => {
+      postModifyPass();
+    });
   };
 
   return (
@@ -262,24 +243,11 @@ const Index: FC = () => {
             type="submit"
             onClick={() => {
               handleFormState();
-              getImgCode();
             }}>
             {t('change_pass_btn')}
           </Button>
         </>
       )}
-
-      <PicAuthCodeModal
-        visible={showModal}
-        data={{
-          captcha: formData.captcha_code,
-          imgCode,
-        }}
-        handleCaptcha={handleChange}
-        clickSubmit={postModifyPass}
-        refreshImgCode={getImgCode}
-        onClose={() => setModalState(false)}
-      />
     </div>
   );
 };
diff --git a/ui/src/services/client/question.ts b/ui/src/services/client/question.ts
index db66462a..92cc5504 100644
--- a/ui/src/services/client/question.ts
+++ b/ui/src/services/client/question.ts
@@ -61,10 +61,15 @@ export const getInviteUser = (questionId: string) => {
   });
 };
 
-export const putInviteUser = (questionId: string, users: string[]) => {
+export const putInviteUser = (
+  questionId: string,
+  users: string[],
+  imgCode: Type.ImgCodeReq = {},
+) => {
   const apiUrl = '/answer/api/v1/question/invite';
   return request.put(apiUrl, {
     id: questionId,
     invite_user: users,
+    ...imgCode,
   });
 };
diff --git a/ui/src/services/client/search.ts b/ui/src/services/client/search.ts
index 8d380294..ce6e9ad4 100644
--- a/ui/src/services/client/search.ts
+++ b/ui/src/services/client/search.ts
@@ -1,20 +1,10 @@
-import useSWR from 'swr';
-import qs from 'qs';
-
 import request from '@/utils/request';
 import type * as Type from '@/common/interface';
 
-export const useSearch = (params?: Type.SearchParams) => {
+export const getSearchResult = (params?: Type.SearchParams) => {
   const apiUrl = '/answer/api/v1/search';
-  const queryParams = qs.stringify(params, { skipNulls: true });
-  const { data, error, mutate } = useSWR<Type.SearchRes, Error>(
-    params?.q ? `${apiUrl}?${queryParams}` : null,
-    request.instance.get,
-  );
-  return {
-    data,
-    isLoading: !data && !error,
-    error,
-    mutate,
-  };
+
+  return request.get<Type.SearchRes>(apiUrl, {
+    params,
+  });
 };
diff --git a/ui/src/services/common.ts b/ui/src/services/common.ts
index cf750a52..bf44dc49 100644
--- a/ui/src/services/common.ts
+++ b/ui/src/services/common.ts
@@ -60,9 +60,10 @@ export const updateComment = (params) => {
   return request.put('/answer/api/v1/comment', params);
 };
 
-export const deleteComment = (id) => {
+export const deleteComment = (id, imgCode: Type.ImgCodeReq = {}) => {
   return request.delete('/answer/api/v1/comment', {
     comment_id: id,
+    ...imgCode,
   });
 };
 
@@ -102,19 +103,10 @@ export const register = (params: Type.RegisterReqParams) => {
   return request.post<any>('/answer/api/v1/user/register/email', params);
 };
 
-export const getRegisterCaptcha = () => {
-  const apiUrl = '/answer/api/v1/user/register/captcha';
-  return request.get(apiUrl);
-};
-
 export const logout = () => {
   return request.get('/answer/api/v1/user/logout');
 };
 
-export const verifyEmail = (code: string) => {
-  return request.get(`/answer/api/v1/email/verify?code=${code}`);
-};
-
 export const resendEmail = (params?: Type.ImgCodeReq) => {
   params = qs.parse(
     qs.stringify(params, {
@@ -134,19 +126,19 @@ export const getLoggedUserInfo = (config = { passingError: false }) => {
   return request.get<Type.UserInfoRes>('/answer/api/v1/user/info', config);
 };
 
-export const modifyPassword = (params: Type.ModifyPasswordReq) => {
-  return request.put('/answer/api/v1/user/password', params);
-};
-
 export const modifyUserInfo = (params: Type.ModifyUserReq) => {
   return request.put('/answer/api/v1/user/info', params);
 };
 
+export const modifyPassword = (params: Type.ModifyPasswordReq) => {
+  return request.put('/answer/api/v1/user/password', params);
+};
+
 export const resetPassword = (params: Type.PasswordResetReq) => {
   return request.post('/answer/api/v1/user/password/reset', params);
 };
 
-export const replacementPassword = (params: { code: string; pass: string }) => {
+export const replacementPassword = (params: Type.PasswordReplaceReq) => {
   return request.post('/answer/api/v1/user/password/replacement', params);
 };
 
@@ -154,10 +146,13 @@ export const activateAccount = (code: string) => {
   return request.post(`/answer/api/v1/user/email/verification`, { code });
 };
 
-export const checkImgCode = (params: Type.CheckImgReq) => {
-  return request.get<Type.ImgCodeRes>(
-    `/answer/api/v1/user/action/record?${qs.stringify(params)}`,
-  );
+export const checkImgCode = (k: Type.CaptchaKey) => {
+  const apiUrl = `/answer/api/v1/user/action/record`;
+  return request.get<Type.ImgCodeRes>(apiUrl, {
+    params: {
+      action: k,
+    },
+  });
 };
 
 export const setNotice = (params: Type.SetNoticeReq) => {
@@ -189,7 +184,7 @@ export const bookmark = (params: { group_id: string; object_id: string }) => {
 };
 
 export const postVote = (
-  params: { object_id: string; is_cancel: boolean },
+  params: { object_id: string; is_cancel: boolean } & Type.ImgCodeReq,
   type: 'down' | 'up',
 ) => {
   return request.post(`/answer/api/v1/vote/${type}`, params);
@@ -224,20 +219,30 @@ export const reportList = ({
   return request.get(`${api}?object_type=${type}&action=${action}`);
 };
 
-export const postReport = (params: {
-  source: Type.ReportType;
-  content: string;
-  object_id: string;
-  report_type: number;
-}) => {
+export const postReport = (
+  params: {
+    source: Type.ReportType;
+    content: string;
+    object_id: string;
+    report_type: number;
+  } & Type.ImgCodeReq,
+) => {
   return request.post('/answer/api/v1/report', params);
 };
 
-export const deleteQuestion = (params: { id: string }) => {
+export const deleteQuestion = (params: {
+  id: string;
+  captcha_code?: string;
+  captcha_id?: string;
+}) => {
   return request.delete('/answer/api/v1/question', params);
 };
 
-export const deleteAnswer = (params: { id: string }) => {
+export const deleteAnswer = (params: {
+  id: string;
+  captcha_code?: string;
+  captcha_id?: string;
+}) => {
   return request.delete('/answer/api/v1/answer', params);
 };
 
diff --git a/ui/src/utils/common.ts b/ui/src/utils/common.ts
index e61e5e7d..efd93e0a 100644
--- a/ui/src/utils/common.ts
+++ b/ui/src/utils/common.ts
@@ -2,8 +2,7 @@ import i18next from 'i18next';
 
 import pattern from '@/common/pattern';
 import { USER_AGENT_NAMES } from '@/common/constants';
-
-const Diff = require('diff');
+import type * as Type from '@/common/interface';
 
 function thousandthDivision(num) {
   const reg = /\d{1,3}(?=(\d{3})+$)/g;
@@ -114,7 +113,7 @@ function escapeRemove(str: string) {
 }
 
 function handleFormError(
-  error: { list: Array<{ error_field: string; error_msg: string }> },
+  error: { list: Type.FieldError[] },
   data: any,
   keymap?: Array<{ from: string; to: string }>,
 ) {
@@ -148,6 +147,8 @@ function escapeHtml(str: string) {
   return str.replace(/[&<>"'`]/g, (tag) => tagsToReplace[tag] || tag);
 }
 
+const Diff = require('diff');
+
 function diffText(newText: string, oldText?: string): string {
   if (!newText) {
     return '';
diff --git a/ui/src/utils/floppyNavigation.ts b/ui/src/utils/floppyNavigation.ts
index 7ed3a77b..707d5c48 100644
--- a/ui/src/utils/floppyNavigation.ts
+++ b/ui/src/utils/floppyNavigation.ts
@@ -94,36 +94,42 @@ export interface NavigateConfig {
 }
 const navigate = (to: string | number, config: NavigateConfig = {}) => {
   let { handler = 'href' } = config;
-  if (to && typeof to === 'string') {
-    if (equalToCurrentHref(to)) {
-      return;
-    }
-    /**
-     * 1. Blocking redirection of two login pages
-     * 2. Auto storage login redirect
-     * Note: The or judgement cannot be missing here, both jumps will be used
-     */
-    if (to === RouteAlias.login || to === getLoginUrl()) {
-      storageLoginRedirect();
+  /**
+   * Note: Synchronised navigation can result in asynchronous actions such as page animations and state modifications not being completed.
+   */
+  setTimeout(() => {
+    if (to && typeof to === 'string') {
+      if (equalToCurrentHref(to)) {
+        return;
+      }
+      /**
+       * 1. Blocking redirection of two login pages
+       * 2. Auto storage login redirect
+       * Note: The or judgement cannot be missing here, both jumps will be used
+       */
+      if (to === RouteAlias.login || to === getLoginUrl()) {
+        storageLoginRedirect();
+      }
+
+      if (!isRoutableLink(to) && handler !== 'href' && handler !== 'replace') {
+        handler = 'href';
+      }
+      if (handler === 'href' && config.options?.replace) {
+        handler = 'replace';
+      }
+      if (handler === 'href') {
+        window.location.href = to;
+      } else if (handler === 'replace') {
+        window.location.replace(to);
+      } else if (typeof handler === 'function') {
+        handler(to, config.options);
+      }
     }
 
-    if (!isRoutableLink(to) && handler !== 'href' && handler !== 'replace') {
-      handler = 'href';
+    if (typeof to === 'number' && typeof handler === 'function') {
+      handler(to);
     }
-    if (handler === 'href' && config.options?.replace) {
-      handler = 'replace';
-    }
-    if (handler === 'href') {
-      window.location.href = to;
-    } else if (handler === 'replace') {
-      window.location.replace(to);
-    } else if (typeof handler === 'function') {
-      handler(to, config.options);
-    }
-  }
-  if (typeof to === 'number' && typeof handler === 'function') {
-    handler(to);
-  }
+  });
 };
 
 /**
diff --git a/ui/src/utils/request.ts b/ui/src/utils/request.ts
index e8cd3b6e..7132099d 100644
--- a/ui/src/utils/request.ts
+++ b/ui/src/utils/request.ts
@@ -61,6 +61,7 @@ class Request {
           config: errConfig,
         } = error.response || {};
         const { data = {}, msg = '' } = errBody || {};
+
         const errorObject: {
           code: any;
           msg: string;
@@ -74,6 +75,7 @@ class Request {
           msg,
           data,
         };
+
         if (status === 400) {
           if (data?.err_type && errConfig?.passingError) {
             return Promise.reject(errorObject);
@@ -127,6 +129,7 @@ class Request {
           floppyNavigation.navigateToLogin();
           return Promise.reject(false);
         }
+
         if (status === 403) {
           // Permission interception
           if (data?.type === 'url_expired') {
@@ -173,6 +176,7 @@ class Request {
           errorCodeStore.getState().update('404');
           return Promise.reject(false);
         }
+
         if (status >= 500) {
           if (isIgnoredPath(IGNORE_PATH_LIST)) {
             return Promise.reject(false);