You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@answer.apache.org by sh...@apache.org on 2024/04/09 03:02:22 UTC

(incubator-answer) branch feat/1.3.1/ui updated (877cfe81 -> cc8dbb9b)

This is an automated email from the ASF dual-hosted git repository.

shuai pushed a change to branch feat/1.3.1/ui
in repository https://gitbox.apache.org/repos/asf/incubator-answer.git


    from 877cfe81 feat: use captcha plugin replace captcha modal component
     add dc757d56 fix: Account setting label issue
     add 3bec091d fix bug where no error message is showing when comment length exceeds max length when updating comment
     add 9853a60c refactor(ui): Refactor flex layout
     add d91cb2e6 fix:fix answer draft dispaly error
     add 1ce78633 feat(ui): Support route types in plugin system
     new aade1d9b feat: use captcha plugin replace captcha modal component
     new cc8dbb9b feat: resolve confict

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 i18n/af_ZA.yaml                                    |  4 +-
 i18n/ar_SA.yaml                                    |  4 +-
 i18n/az_AZ.yaml                                    |  4 +-
 i18n/bal_BA.yaml                                   |  4 +-
 i18n/ban_ID.yaml                                   |  4 +-
 i18n/bn_BD.yaml                                    |  4 +-
 i18n/bs_BA.yaml                                    |  4 +-
 i18n/ca_ES.yaml                                    |  4 +-
 i18n/cs_CZ.yaml                                    |  2 +
 i18n/cy_GB.yaml                                    |  2 +
 i18n/da_DK.yaml                                    |  2 +
 i18n/de_DE.yaml                                    |  2 +
 i18n/el_GR.yaml                                    |  4 +-
 i18n/en_US.yaml                                    |  2 +
 i18n/es_ES.yaml                                    |  2 +
 i18n/fi_FI.yaml                                    |  4 +-
 i18n/he_IL.yaml                                    |  4 +-
 i18n/hi_IN.yaml                                    |  2 +
 i18n/hu_HU.yaml                                    |  4 +-
 i18n/hy_AM.yaml                                    |  4 +-
 i18n/id_ID.yaml                                    |  2 +
 i18n/ja_JP.yaml                                    |  2 +
 i18n/ko_KR.yaml                                    |  2 +
 i18n/nl_NL.yaml                                    |  4 +-
 i18n/no_NO.yaml                                    |  4 +-
 i18n/sk_SK.yaml                                    |  2 +
 i18n/sq_AL.yaml                                    |  4 +-
 i18n/sr_SP.yaml                                    |  4 +-
 i18n/te_IN.yaml                                    |  2 +
 i18n/tr_TR.yaml                                    |  2 +
 i18n/vi_VN.yaml                                    |  2 +
 i18n/zh_CN.yaml                                    |  2 +
 i18n/zh_TW.yaml                                    |  2 +
 ui/src/components/Comment/index.tsx                |  7 +-
 ui/src/components/SideNav/index.scss               |  1 -
 ui/src/pages/Layout/index.tsx                      |  2 +-
 .../Detail/components/WriteAnswer/index.tsx        | 22 +++---
 ui/src/pages/SideNavLayout/index.tsx               |  4 +-
 .../Account/components/ModifyEmail/index.tsx       |  4 +-
 ui/src/plugins/builtin/HostingConnector/index.tsx  |  7 +-
 ui/src/plugins/builtin/SearchInfo/index.tsx        |  7 +-
 .../plugins/builtin/ThirdPartyConnector/index.tsx  |  7 +-
 ui/src/router/index.tsx                            |  4 +-
 ui/src/utils/pluginKit/index.ts                    | 80 ++++++++++++++++------
 ui/src/utils/pluginKit/utils.ts                    | 28 +++++++-
 45 files changed, 212 insertions(+), 61 deletions(-)


(incubator-answer) 02/02: feat: resolve confict

Posted by sh...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

shuai pushed a commit to branch feat/1.3.1/ui
in repository https://gitbox.apache.org/repos/asf/incubator-answer.git

commit cc8dbb9b1cb229bd8fca9fb0165807b5853b2ba6
Merge: aade1d9b 877cfe81
Author: shuai <li...@sifou.com>
AuthorDate: Tue Apr 9 11:02:09 2024 +0800

    feat: resolve confict

 ui/src/components/PluginRender/index.tsx |  3 ++-
 ui/src/utils/pluginKit/interface.ts      | 27 +++++++++++++++++++++++++++
 2 files changed, 29 insertions(+), 1 deletion(-)


(incubator-answer) 01/02: feat: use captcha plugin replace captcha modal component

Posted by sh...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

shuai pushed a commit to branch feat/1.3.1/ui
in repository https://gitbox.apache.org/repos/asf/incubator-answer.git

commit aade1d9baffb1a6dfb40fd92aaf86f4f318474cf
Author: shuai <li...@sifou.com>
AuthorDate: Tue Apr 9 10:15:22 2024 +0800

    feat: use captcha plugin replace captcha modal component
---
 .vscode/settings.json                              |   5 +-
 i18n/af_ZA.yaml                                    |   2 +-
 i18n/ar_SA.yaml                                    |   2 +-
 i18n/az_AZ.yaml                                    |   2 +-
 i18n/bal_BA.yaml                                   |   2 +-
 i18n/ban_ID.yaml                                   |   2 +-
 i18n/bn_BD.yaml                                    |   2 +-
 i18n/bs_BA.yaml                                    |   2 +-
 i18n/ca_ES.yaml                                    |   2 +-
 i18n/cs_CZ.yaml                                    |   2 +-
 i18n/cy_GB.yaml                                    |   2 +-
 i18n/da_DK.yaml                                    |   2 +-
 i18n/el_GR.yaml                                    |   2 +-
 i18n/en_US.yaml                                    |   2 +-
 i18n/fi_FI.yaml                                    |   2 +-
 i18n/he_IL.yaml                                    |   2 +-
 i18n/hi_IN.yaml                                    |   2 +-
 i18n/hu_HU.yaml                                    |   2 +-
 i18n/hy_AM.yaml                                    |   2 +-
 i18n/id_ID.yaml                                    |   2 +-
 i18n/ja_JP.yaml                                    |   8 +-
 i18n/ko_KR.yaml                                    |   2 +-
 i18n/nl_NL.yaml                                    |   2 +-
 i18n/no_NO.yaml                                    |   2 +-
 i18n/sq_AL.yaml                                    |   2 +-
 i18n/sr_SP.yaml                                    |   2 +-
 i18n/sv_SE.yaml                                    |   2 +-
 i18n/te_IN.yaml                                    |   2 +-
 i18n/tr_TR.yaml                                    |   2 +-
 i18n/uk_UA.yaml                                    |   2 +-
 i18n/vi_VN.yaml                                    |   2 +-
 ui/src/components/Actions/index.tsx                |  80 +++---
 ui/src/components/Comment/index.tsx                | 284 +++++++++++----------
 ui/src/components/Operate/index.tsx                | 107 ++++----
 ui/src/components/Unactivate/index.tsx             |  16 +-
 ui/src/hooks/useReportModal/index.tsx              |  63 +++--
 ui/src/pages/Questions/Ask/index.tsx               | 159 ++++++------
 .../Detail/components/InviteToAnswer/index.tsx     |  47 ++--
 .../Detail/components/WriteAnswer/index.tsx        |  78 +++---
 ui/src/pages/Questions/EditAnswer/index.tsx        |  76 +++---
 .../Review/components/ApproveDropdown/index.tsx    |  89 ++++---
 .../Review/components/EditPostModal/index.tsx      |  69 ++---
 ui/src/pages/Search/index.tsx                      |  15 +-
 .../Users/AccountForgot/components/sendEmail.tsx   |  17 +-
 .../Users/ChangeEmail/components/sendEmail.tsx     |  17 +-
 ui/src/pages/Users/Login/index.tsx                 |  29 ++-
 .../Users/Register/components/SignUpForm/index.tsx |  14 +-
 .../Account/components/ModifyEmail/index.tsx       |  18 +-
 .../Account/components/ModifyPass/index.tsx        |  17 +-
 ui/src/plugins/builtin/HostingConnector/index.tsx  |   7 +-
 ui/src/plugins/builtin/SearchInfo/index.tsx        |   7 +-
 .../plugins/builtin/ThirdPartyConnector/index.tsx  |   7 +-
 ui/src/router/pathFactory.ts                       |   2 +-
 ui/src/utils/pluginKit/index.ts                    |  17 +-
 ui/src/utils/pluginKit/utils.ts                    |  16 +-
 55 files changed, 763 insertions(+), 559 deletions(-)

diff --git a/.vscode/settings.json b/.vscode/settings.json
index db563c27..b7d39e6e 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -2,5 +2,8 @@
   "eslint.workingDirectories": [
     "ui"
   ],
-  "explorer.autoReveal": "focusNoScroll"
+  "explorer.autoReveal": "focusNoScroll",
+  "cSpell.words": [
+    "grecaptcha"
+  ]
 }
diff --git a/i18n/af_ZA.yaml b/i18n/af_ZA.yaml
index 71162ef5..8c6d0d7d 100644
--- a/i18n/af_ZA.yaml
+++ b/i18n/af_ZA.yaml
@@ -813,7 +813,7 @@ ui:
       more: More
     tips:
       title: Advanced Search Tips
-      tag: "<1>[tag]</1> search withing a tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> search by author"
       answer: "<1>answers:0</1> unanswered questions"
       score: "<1>score:3</1> posts with a 3+ score"
diff --git a/i18n/ar_SA.yaml b/i18n/ar_SA.yaml
index 2f686840..39b63e8c 100644
--- a/i18n/ar_SA.yaml
+++ b/i18n/ar_SA.yaml
@@ -813,7 +813,7 @@ ui:
       more: More
     tips:
       title: Advanced Search Tips
-      tag: "<1>[tag]</1> search withing a tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> search by author"
       answer: "<1>answers:0</1> unanswered questions"
       score: "<1>score:3</1> posts with a 3+ score"
diff --git a/i18n/az_AZ.yaml b/i18n/az_AZ.yaml
index 4270506a..07e50541 100644
--- a/i18n/az_AZ.yaml
+++ b/i18n/az_AZ.yaml
@@ -805,7 +805,7 @@ ui:
       more: More
     tips:
       title: Advanced Search Tips
-      tag: "<1>[tag]</1> search withing a tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> search by author"
       answer: "<1>answers:0</1> unanswered questions"
       score: "<1>score:3</1> posts with a 3+ score"
diff --git a/i18n/bal_BA.yaml b/i18n/bal_BA.yaml
index 4270506a..07e50541 100644
--- a/i18n/bal_BA.yaml
+++ b/i18n/bal_BA.yaml
@@ -805,7 +805,7 @@ ui:
       more: More
     tips:
       title: Advanced Search Tips
-      tag: "<1>[tag]</1> search withing a tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> search by author"
       answer: "<1>answers:0</1> unanswered questions"
       score: "<1>score:3</1> posts with a 3+ score"
diff --git a/i18n/ban_ID.yaml b/i18n/ban_ID.yaml
index 4270506a..07e50541 100644
--- a/i18n/ban_ID.yaml
+++ b/i18n/ban_ID.yaml
@@ -805,7 +805,7 @@ ui:
       more: More
     tips:
       title: Advanced Search Tips
-      tag: "<1>[tag]</1> search withing a tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> search by author"
       answer: "<1>answers:0</1> unanswered questions"
       score: "<1>score:3</1> posts with a 3+ score"
diff --git a/i18n/bn_BD.yaml b/i18n/bn_BD.yaml
index 4270506a..07e50541 100644
--- a/i18n/bn_BD.yaml
+++ b/i18n/bn_BD.yaml
@@ -805,7 +805,7 @@ ui:
       more: More
     tips:
       title: Advanced Search Tips
-      tag: "<1>[tag]</1> search withing a tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> search by author"
       answer: "<1>answers:0</1> unanswered questions"
       score: "<1>score:3</1> posts with a 3+ score"
diff --git a/i18n/bs_BA.yaml b/i18n/bs_BA.yaml
index 4270506a..07e50541 100644
--- a/i18n/bs_BA.yaml
+++ b/i18n/bs_BA.yaml
@@ -805,7 +805,7 @@ ui:
       more: More
     tips:
       title: Advanced Search Tips
-      tag: "<1>[tag]</1> search withing a tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> search by author"
       answer: "<1>answers:0</1> unanswered questions"
       score: "<1>score:3</1> posts with a 3+ score"
diff --git a/i18n/ca_ES.yaml b/i18n/ca_ES.yaml
index 2f686840..39b63e8c 100644
--- a/i18n/ca_ES.yaml
+++ b/i18n/ca_ES.yaml
@@ -813,7 +813,7 @@ ui:
       more: More
     tips:
       title: Advanced Search Tips
-      tag: "<1>[tag]</1> search withing a tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> search by author"
       answer: "<1>answers:0</1> unanswered questions"
       score: "<1>score:3</1> posts with a 3+ score"
diff --git a/i18n/cs_CZ.yaml b/i18n/cs_CZ.yaml
index 3e5bc495..0a260a04 100644
--- a/i18n/cs_CZ.yaml
+++ b/i18n/cs_CZ.yaml
@@ -1178,7 +1178,7 @@ ui:
       more: More
     tips:
       title: Advanced Search Tips
-      tag: "<1>[tag]</1> search withing a tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> search by author"
       answer: "<1>answers:0</1> unanswered questions"
       score: "<1>score:3</1> posts with a 3+ score"
diff --git a/i18n/cy_GB.yaml b/i18n/cy_GB.yaml
index 8825ee2b..a148a9fa 100644
--- a/i18n/cy_GB.yaml
+++ b/i18n/cy_GB.yaml
@@ -1178,7 +1178,7 @@ ui:
       more: More
     tips:
       title: Advanced Search Tips
-      tag: "<1>[tag]</1> search withing a tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> search by author"
       answer: "<1>answers:0</1> unanswered questions"
       score: "<1>score:3</1> posts with a 3+ score"
diff --git a/i18n/da_DK.yaml b/i18n/da_DK.yaml
index 88110a3e..c210b188 100644
--- a/i18n/da_DK.yaml
+++ b/i18n/da_DK.yaml
@@ -1178,7 +1178,7 @@ ui:
       more: More
     tips:
       title: Advanced Search Tips
-      tag: "<1>[tag]</1> search withing a tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> search by author"
       answer: "<1>answers:0</1> unanswered questions"
       score: "<1>score:3</1> posts with a 3+ score"
diff --git a/i18n/el_GR.yaml b/i18n/el_GR.yaml
index 2f686840..39b63e8c 100644
--- a/i18n/el_GR.yaml
+++ b/i18n/el_GR.yaml
@@ -813,7 +813,7 @@ ui:
       more: More
     tips:
       title: Advanced Search Tips
-      tag: "<1>[tag]</1> search withing a tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> search by author"
       answer: "<1>answers:0</1> unanswered questions"
       score: "<1>score:3</1> posts with a 3+ score"
diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml
index 0221cfa0..ee47b3e1 100644
--- a/i18n/en_US.yaml
+++ b/i18n/en_US.yaml
@@ -1215,7 +1215,7 @@ ui:
       more: More
     tips:
       title: Advanced Search Tips
-      tag: "<1>[tag]</1> search withing a tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> search by author"
       answer: "<1>answers:0</1> unanswered questions"
       score: "<1>score:3</1> posts with a 3+ score"
diff --git a/i18n/fi_FI.yaml b/i18n/fi_FI.yaml
index 2f686840..39b63e8c 100644
--- a/i18n/fi_FI.yaml
+++ b/i18n/fi_FI.yaml
@@ -813,7 +813,7 @@ ui:
       more: More
     tips:
       title: Advanced Search Tips
-      tag: "<1>[tag]</1> search withing a tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> search by author"
       answer: "<1>answers:0</1> unanswered questions"
       score: "<1>score:3</1> posts with a 3+ score"
diff --git a/i18n/he_IL.yaml b/i18n/he_IL.yaml
index 2f686840..39b63e8c 100644
--- a/i18n/he_IL.yaml
+++ b/i18n/he_IL.yaml
@@ -813,7 +813,7 @@ ui:
       more: More
     tips:
       title: Advanced Search Tips
-      tag: "<1>[tag]</1> search withing a tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> search by author"
       answer: "<1>answers:0</1> unanswered questions"
       score: "<1>score:3</1> posts with a 3+ score"
diff --git a/i18n/hi_IN.yaml b/i18n/hi_IN.yaml
index 88110a3e..c210b188 100644
--- a/i18n/hi_IN.yaml
+++ b/i18n/hi_IN.yaml
@@ -1178,7 +1178,7 @@ ui:
       more: More
     tips:
       title: Advanced Search Tips
-      tag: "<1>[tag]</1> search withing a tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> search by author"
       answer: "<1>answers:0</1> unanswered questions"
       score: "<1>score:3</1> posts with a 3+ score"
diff --git a/i18n/hu_HU.yaml b/i18n/hu_HU.yaml
index 2f686840..39b63e8c 100644
--- a/i18n/hu_HU.yaml
+++ b/i18n/hu_HU.yaml
@@ -813,7 +813,7 @@ ui:
       more: More
     tips:
       title: Advanced Search Tips
-      tag: "<1>[tag]</1> search withing a tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> search by author"
       answer: "<1>answers:0</1> unanswered questions"
       score: "<1>score:3</1> posts with a 3+ score"
diff --git a/i18n/hy_AM.yaml b/i18n/hy_AM.yaml
index 4270506a..07e50541 100644
--- a/i18n/hy_AM.yaml
+++ b/i18n/hy_AM.yaml
@@ -805,7 +805,7 @@ ui:
       more: More
     tips:
       title: Advanced Search Tips
-      tag: "<1>[tag]</1> search withing a tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> search by author"
       answer: "<1>answers:0</1> unanswered questions"
       score: "<1>score:3</1> posts with a 3+ score"
diff --git a/i18n/id_ID.yaml b/i18n/id_ID.yaml
index e9277a5e..e0278c46 100644
--- a/i18n/id_ID.yaml
+++ b/i18n/id_ID.yaml
@@ -1178,7 +1178,7 @@ ui:
       more: More
     tips:
       title: Advanced Search Tips
-      tag: "<1>[tag]</1> search withing a tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> search by author"
       answer: "<1>answers:0</1> unanswered questions"
       score: "<1>score:3</1> posts with a 3+ score"
diff --git a/i18n/ja_JP.yaml b/i18n/ja_JP.yaml
index bfb9b032..f4a290ac 100644
--- a/i18n/ja_JP.yaml
+++ b/i18n/ja_JP.yaml
@@ -518,7 +518,7 @@ ui:
     edit_tag: タグを編集
     ask_a_question: 質問を追加
     edit_question: 質問を編集
-    edit_answer: 回答を編集 
+    edit_answer: 回答を編集
     search: 検索
     posts_containing: Posts containing
     settings: 設定
@@ -763,8 +763,8 @@ ui:
       Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting.
     tip_vote: It adds something useful to the post
   edit_answer:
-    title: 回答を編集 
-    default_reason: 回答を編集 
+    title: 回答を編集
+    default_reason: 回答を編集
     default_first_reason: 回答を追加
     form:
       fields:
@@ -1178,7 +1178,7 @@ ui:
       more: More
     tips:
       title: Advanced Search Tips
-      tag: "<1>[tag]</1> search withing a tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> search by author"
       answer: "<1>answers:0</1> unanswered questions"
       score: "<1>score:3</1> posts with a 3+ score"
diff --git a/i18n/ko_KR.yaml b/i18n/ko_KR.yaml
index f0bc42c8..d04af1b5 100644
--- a/i18n/ko_KR.yaml
+++ b/i18n/ko_KR.yaml
@@ -1178,7 +1178,7 @@ ui:
       more: More
     tips:
       title: Advanced Search Tips
-      tag: "<1>[tag]</1> search withing a tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> search by author"
       answer: "<1>answers:0</1> unanswered questions"
       score: "<1>score:3</1> posts with a 3+ score"
diff --git a/i18n/nl_NL.yaml b/i18n/nl_NL.yaml
index 2f686840..39b63e8c 100644
--- a/i18n/nl_NL.yaml
+++ b/i18n/nl_NL.yaml
@@ -813,7 +813,7 @@ ui:
       more: More
     tips:
       title: Advanced Search Tips
-      tag: "<1>[tag]</1> search withing a tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> search by author"
       answer: "<1>answers:0</1> unanswered questions"
       score: "<1>score:3</1> posts with a 3+ score"
diff --git a/i18n/no_NO.yaml b/i18n/no_NO.yaml
index 2f686840..39b63e8c 100644
--- a/i18n/no_NO.yaml
+++ b/i18n/no_NO.yaml
@@ -813,7 +813,7 @@ ui:
       more: More
     tips:
       title: Advanced Search Tips
-      tag: "<1>[tag]</1> search withing a tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> search by author"
       answer: "<1>answers:0</1> unanswered questions"
       score: "<1>score:3</1> posts with a 3+ score"
diff --git a/i18n/sq_AL.yaml b/i18n/sq_AL.yaml
index 4270506a..07e50541 100644
--- a/i18n/sq_AL.yaml
+++ b/i18n/sq_AL.yaml
@@ -805,7 +805,7 @@ ui:
       more: More
     tips:
       title: Advanced Search Tips
-      tag: "<1>[tag]</1> search withing a tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> search by author"
       answer: "<1>answers:0</1> unanswered questions"
       score: "<1>score:3</1> posts with a 3+ score"
diff --git a/i18n/sr_SP.yaml b/i18n/sr_SP.yaml
index 2f686840..39b63e8c 100644
--- a/i18n/sr_SP.yaml
+++ b/i18n/sr_SP.yaml
@@ -813,7 +813,7 @@ ui:
       more: More
     tips:
       title: Advanced Search Tips
-      tag: "<1>[tag]</1> search withing a tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> search by author"
       answer: "<1>answers:0</1> unanswered questions"
       score: "<1>score:3</1> posts with a 3+ score"
diff --git a/i18n/sv_SE.yaml b/i18n/sv_SE.yaml
index 7cef6932..2030c951 100644
--- a/i18n/sv_SE.yaml
+++ b/i18n/sv_SE.yaml
@@ -1176,7 +1176,7 @@ ui:
       more: More
     tips:
       title: Advanced Search Tips
-      tag: "<1>[tag]</1> search withing a tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> search by author"
       answer: "<1>answers:0</1> unanswered questions"
       score: "<1>score:3</1> posts with a 3+ score"
diff --git a/i18n/te_IN.yaml b/i18n/te_IN.yaml
index 8be7a385..bc507023 100644
--- a/i18n/te_IN.yaml
+++ b/i18n/te_IN.yaml
@@ -1178,7 +1178,7 @@ ui:
       more: More
     tips:
       title: Advanced Search Tips
-      tag: "<1>[tag]</1> search withing a tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> search by author"
       answer: "<1>answers:0</1> unanswered questions"
       score: "<1>score:3</1> posts with a 3+ score"
diff --git a/i18n/tr_TR.yaml b/i18n/tr_TR.yaml
index c9cfe1c9..7a9cc591 100644
--- a/i18n/tr_TR.yaml
+++ b/i18n/tr_TR.yaml
@@ -1178,7 +1178,7 @@ ui:
       more: More
     tips:
       title: Advanced Search Tips
-      tag: "<1>[tag]</1> search withing a tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> search by author"
       answer: "<1>answers:0</1> unanswered questions"
       score: "<1>score:3</1> posts with a 3+ score"
diff --git a/i18n/uk_UA.yaml b/i18n/uk_UA.yaml
index 21421f66..acd8b2f8 100644
--- a/i18n/uk_UA.yaml
+++ b/i18n/uk_UA.yaml
@@ -1176,7 +1176,7 @@ ui:
       more: More
     tips:
       title: Advanced Search Tips
-      tag: "<1>[tag]</1> search withing a tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> search by author"
       answer: "<1>answers:0</1> unanswered questions"
       score: "<1>score:3</1> posts with a 3+ score"
diff --git a/i18n/vi_VN.yaml b/i18n/vi_VN.yaml
index acf8084c..925a1434 100644
--- a/i18n/vi_VN.yaml
+++ b/i18n/vi_VN.yaml
@@ -1178,7 +1178,7 @@ ui:
       more: More
     tips:
       title: Advanced Search Tips
-      tag: "<1>[tag]</1> search withing a tag"
+      tag: "<1>[tag]</1> search with a tag"
       user: "<1>user:username</1> search by author"
       answer: "<1>answers:0</1> unanswered questions"
       score: "<1>score:3</1> posts with a 3+ score"
diff --git a/ui/src/components/Actions/index.tsx b/ui/src/components/Actions/index.tsx
index b8357105..b58d0c8c 100644
--- a/ui/src/components/Actions/index.tsx
+++ b/ui/src/components/Actions/index.tsx
@@ -25,7 +25,8 @@ import classNames from 'classnames';
 
 import { Icon } from '@/components';
 import { loggedUserInfoStore } from '@/stores';
-import { useToast, useCaptchaModal } from '@/hooks';
+import { useToast } from '@/hooks';
+import { useCaptchaPlugin } from '@/utils/pluginKit';
 import { tryNormalLogged } from '@/utils/guard';
 import { bookmark, postVote } from '@/services';
 import * as Types from '@/common/interface';
@@ -56,7 +57,7 @@ const Index: FC<Props> = ({ className, data, source }) => {
   const { username = '' } = loggedUserInfoStore((state) => state.user);
   const toast = useToast();
   const { t } = useTranslation();
-  const vCaptcha = useCaptchaModal('vote');
+  const vCaptcha = useCaptchaPlugin('vote');
 
   useEffect(() => {
     if (data) {
@@ -70,6 +71,42 @@ const Index: FC<Props> = ({ className, data, source }) => {
     }
   }, []);
 
+  const submitVote = (type) => {
+    const isCancel = (type === 'up' && like) || (type === 'down' && hate);
+    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 handleVote = (type: 'up' | 'down') => {
     if (!tryNormalLogged(true)) {
       return;
@@ -82,39 +119,14 @@ const Index: FC<Props> = ({ className, data, source }) => {
       });
       return;
     }
-    const isCancel = (type === 'up' && like) || (type === 'down' && hate);
+
+    if (!vCaptcha) {
+      submitVote(type);
+      return;
+    }
+
     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',
-            });
-          }
-        });
+      submitVote(type);
     });
   };
 
diff --git a/ui/src/components/Comment/index.tsx b/ui/src/components/Comment/index.tsx
index 06ecfc26..ca69b2de 100644
--- a/ui/src/components/Comment/index.tsx
+++ b/ui/src/components/Comment/index.tsx
@@ -35,6 +35,7 @@ import {
   bgFadeOut,
 } from '@/utils';
 import { tryNormalLogged } from '@/utils/guard';
+import { useCaptchaPlugin } from '@/utils/pluginKit';
 import {
   useQueryComments,
   addComment,
@@ -66,9 +67,9 @@ 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 editCaptcha = useCaptchaPlugin('edit');
+  const dCaptcha = useCaptchaPlugin('delete');
+  const vCaptcha = useCaptchaPlugin('vote');
 
   const { t } = useTranslation('translation', { keyPrefix: 'comment' });
 
@@ -136,6 +137,85 @@ const Comment = ({ objectId, mode, commentId }) => {
     );
   };
 
+  const submitUpdateComment = (params, item) => {
+    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) {
+          const captchaErr = editCaptcha?.handleCaptchaError(err.list);
+          // If it is not a CAPTCHA error, leave it to the subsequent error handling logic to continue processing.
+          if (!(captchaErr && err.list.length === 1)) {
+            return Promise.reject(err);
+          }
+        }
+        return Promise.resolve();
+      });
+  };
+
+  const submitAddComment = (params, item) => {
+    const req = {
+      ...params,
+      captcha_code: undefined,
+      captcha_id: undefined,
+    };
+    addCaptcha?.resolveCaptchaReq(req);
+
+    return addComment(req)
+      .then(async (res) => {
+        await addCaptcha?.close();
+        if (item.type === 'reply') {
+          const index = comments.findIndex(
+            (comment) => comment.comment_id === item.comment_id,
+          );
+          updateCurrentReplyId('');
+          comments.splice(index + 1, 0, res);
+          setComments([...comments]);
+        } else {
+          setComments([
+            ...comments.map((comment) => {
+              if (comment.comment_id === item.comment_id) {
+                updateCurrentReplyId('');
+              }
+              return comment;
+            }),
+            res,
+          ]);
+        }
+
+        setVisibleComment(false);
+      })
+      .catch((ex) => {
+        if (ex.isError) {
+          const captchaErr = addCaptcha?.handleCaptchaError(ex.list);
+          // If it is not a CAPTCHA error, leave it to the subsequent error handling logic to continue processing.
+          if (!(captchaErr && ex.list.length === 1)) {
+            return Promise.reject(ex);
+          }
+        }
+        return Promise.resolve();
+      });
+  };
+
   const handleSendReply = (item) => {
     const users = matchedUsers(item.value);
     const userNames = unionBy(users.map((user) => user.userName));
@@ -153,85 +233,36 @@ const Comment = ({ objectId, mode, commentId }) => {
     };
 
     if (item.type === 'edit') {
-      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) {
-              const captchaErr = editCaptcha.handleCaptchaError(err.list);
-              // If it is not a CAPTCHA error, leave it to the subsequent error handling logic to continue processing.
-              if (!(captchaErr && err.list.length === 1)) {
-                return Promise.reject(err);
-              }
-            }
-            return Promise.resolve();
-          });
-      });
+      if (!editCaptcha) {
+        return submitUpdateComment(params, item);
+      }
+      return editCaptcha.check(() => submitUpdateComment(params, item));
     }
 
-    return addCaptcha.check(() => {
-      const req = {
-        ...params,
-        captcha_code: undefined,
-        captcha_id: undefined,
-      };
-      addCaptcha.resolveCaptchaReq(req);
-
-      return addComment(req)
-        .then(async (res) => {
-          await addCaptcha.close();
-          if (item.type === 'reply') {
-            const index = comments.findIndex(
-              (comment) => comment.comment_id === item.comment_id,
-            );
-            updateCurrentReplyId('');
-            comments.splice(index + 1, 0, res);
-            setComments([...comments]);
-          } else {
-            setComments([
-              ...comments.map((comment) => {
-                if (comment.comment_id === item.comment_id) {
-                  updateCurrentReplyId('');
-                }
-                return comment;
-              }),
-              res,
-            ]);
-          }
+    if (!addCaptcha) {
+      return submitAddComment(params, item);
+    }
 
-          setVisibleComment(false);
-        })
-        .catch((ex) => {
-          if (ex.isError) {
-            const captchaErr = addCaptcha.handleCaptchaError(ex.list);
-            // If it is not a CAPTCHA error, leave it to the subsequent error handling logic to continue processing.
-            if (!(captchaErr && ex.list.length === 1)) {
-              return Promise.reject(ex);
-            }
-          }
-          return Promise.resolve();
-        });
-    });
+    return addCaptcha.check(() => submitAddComment(params, item));
+  };
+
+  const submitDeleteComment = (id) => {
+    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);
+        }
+      });
   };
 
   const handleDelete = (id) => {
@@ -241,67 +272,64 @@ const Comment = ({ objectId, mode, commentId }) => {
       confirmBtnVariant: 'danger',
       confirmText: t('delete', { keyPrefix: 'btns' }),
       onConfirm: () => {
+        if (!dCaptcha) {
+          submitDeleteComment(id);
+          return;
+        }
         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);
-              }
-            });
+          submitDeleteComment(id);
         });
       },
     });
   };
 
+  const submitVoteComment = (id, is_cancel) => {
+    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);
+        }
+      });
+  };
   const handleVote = (id, is_cancel) => {
     if (!tryNormalLogged(true)) {
       return;
     }
 
+    if (!vCaptcha) {
+      submitVoteComment(id, is_cancel);
+      return;
+    }
+
     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);
-          }
-        });
+      submitVoteComment(id, is_cancel);
     });
   };
 
diff --git a/ui/src/components/Operate/index.tsx b/ui/src/components/Operate/index.tsx
index e04416f2..47b4463f 100644
--- a/ui/src/components/Operate/index.tsx
+++ b/ui/src/components/Operate/index.tsx
@@ -23,7 +23,8 @@ import { Link, useNavigate } from 'react-router-dom';
 import { useTranslation } from 'react-i18next';
 
 import { Modal } from '@/components';
-import { useReportModal, useToast, useCaptchaModal } from '@/hooks';
+import { useReportModal, useToast } from '@/hooks';
+import { useCaptchaPlugin } from '@/utils/pluginKit';
 import { QuestionOperationReq } from '@/common/interface';
 import Share from '../Share';
 import {
@@ -63,7 +64,7 @@ const Index: FC<IProps> = ({
   const toast = useToast();
   const navigate = useNavigate();
   const reportModal = useReportModal();
-  const dCaptcha = useCaptchaModal('delete');
+  const dCaptcha = useCaptchaPlugin('delete');
 
   const refreshQuestion = () => {
     callback?.('default');
@@ -88,6 +89,55 @@ const Index: FC<IProps> = ({
     });
   };
 
+  const submitDeleteQuestion = () => {
+    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);
+        }
+      });
+  };
+
+  const submitDeleteAnswer = () => {
+    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?.('delete_answer');
+      })
+      .catch((ex) => {
+        if (ex.isError) {
+          dCaptcha?.handleCaptchaError(ex.list);
+        }
+      });
+  };
+
   const handleDelete = () => {
     if (type === 'question') {
       Modal.confirm({
@@ -97,28 +147,12 @@ const Index: FC<IProps> = ({
         confirmBtnVariant: 'danger',
         confirmText: t('delete', { keyPrefix: 'btns' }),
         onConfirm: () => {
+          if (!dCaptcha) {
+            submitDeleteQuestion();
+            return;
+          }
           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);
-                }
-              });
+            submitDeleteQuestion();
           });
         },
       });
@@ -132,29 +166,12 @@ const Index: FC<IProps> = ({
         confirmBtnVariant: 'danger',
         confirmText: t('delete', { keyPrefix: 'btns' }),
         onConfirm: () => {
+          if (!dCaptcha) {
+            submitDeleteAnswer();
+            return;
+          }
           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?.('delete_answer');
-              })
-              .catch((ex) => {
-                if (ex.isError) {
-                  dCaptcha.handleCaptchaError(ex.list);
-                }
-              });
+            submitDeleteAnswer();
           });
         },
       });
diff --git a/ui/src/components/Unactivate/index.tsx b/ui/src/components/Unactivate/index.tsx
index e96e2875..163da2bd 100644
--- a/ui/src/components/Unactivate/index.tsx
+++ b/ui/src/components/Unactivate/index.tsx
@@ -26,7 +26,7 @@ import type { ImgCodeReq, FormDataType } from '@/common/interface';
 import { loggedUserInfoStore } from '@/stores';
 import { resendEmail } from '@/services';
 import { handleFormError } from '@/utils';
-import { useCaptchaModal } from '@/hooks';
+import { useCaptchaPlugin } from '@/utils/pluginKit';
 
 interface IProps {
   visible?: boolean;
@@ -44,12 +44,12 @@ const Index: React.FC<IProps> = () => {
     },
   });
 
-  const emailCaptcha = useCaptchaModal('email');
+  const emailCaptcha = useCaptchaPlugin('email');
 
   const submit = () => {
     let req: ImgCodeReq = {};
-    const imgCode = emailCaptcha.getCaptcha();
-    if (imgCode.verify) {
+    const imgCode = emailCaptcha?.getCaptcha();
+    if (imgCode?.verify) {
       req = {
         captcha_code: imgCode.captcha_code,
         captcha_id: imgCode.captcha_id,
@@ -57,12 +57,12 @@ const Index: React.FC<IProps> = () => {
     }
     resendEmail(req)
       .then(async () => {
-        await emailCaptcha.close();
+        await emailCaptcha?.close();
         setSuccess(true);
       })
       .catch((err) => {
         if (err.isError) {
-          emailCaptcha.handleCaptchaError(err.list);
+          emailCaptcha?.handleCaptchaError(err.list);
           const data = handleFormError(err, formData);
           setFormData({ ...data });
         }
@@ -71,6 +71,10 @@ const Index: React.FC<IProps> = () => {
 
   const onSentEmail = (evt) => {
     evt.preventDefault();
+    if (!emailCaptcha) {
+      submit();
+      return;
+    }
     emailCaptcha.check(() => {
       submit();
     });
diff --git a/ui/src/hooks/useReportModal/index.tsx b/ui/src/hooks/useReportModal/index.tsx
index e20b8eb6..843472cd 100644
--- a/ui/src/hooks/useReportModal/index.tsx
+++ b/ui/src/hooks/useReportModal/index.tsx
@@ -23,7 +23,8 @@ import { useTranslation } from 'react-i18next';
 
 import ReactDOM from 'react-dom/client';
 
-import { useToast, useCaptchaModal } from '@/hooks';
+import { useToast } from '@/hooks';
+import { useCaptchaPlugin } from '@/utils/pluginKit';
 import type * as Type from '@/common/interface';
 import {
   reportList,
@@ -65,7 +66,7 @@ const useReportModal = (callback?: () => void) => {
   const [show, setShow] = useState(false);
   const [list, setList] = useState<any[]>([]);
 
-  const rCaptcha = useCaptchaModal('report');
+  const rCaptcha = useCaptchaPlugin('report');
 
   useEffect(() => {
     const div = document.createElement('div');
@@ -168,6 +169,34 @@ const useReportModal = (callback?: () => void) => {
     return true;
   };
 
+  const submitReport = (data) => {
+    const flagReq = {
+      source: data.type,
+      report_type: reportType.type,
+      object_id: data.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);
+        }
+      });
+  };
+
   const handleSubmit = () => {
     if (!params) {
       return;
@@ -205,32 +234,12 @@ const useReportModal = (callback?: () => void) => {
       return;
     }
     if (!params.isBackend && params.action === 'flag') {
+      if (!rCaptcha) {
+        submitReport(params);
+        return;
+      }
       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);
-            }
-          });
+        submitReport(params);
       });
     }
 
diff --git a/ui/src/pages/Questions/Ask/index.tsx b/ui/src/pages/Questions/Ask/index.tsx
index 9a1ff14a..367ccfca 100644
--- a/ui/src/pages/Questions/Ask/index.tsx
+++ b/ui/src/pages/Questions/Ask/index.tsx
@@ -27,7 +27,7 @@ import classNames from 'classnames';
 import isEqual from 'lodash/isEqual';
 import debounce from 'lodash/debounce';
 
-import { usePageTags, usePromptWithUnload, useCaptchaModal } from '@/hooks';
+import { usePageTags, usePromptWithUnload } from '@/hooks';
 import { Editor, EditorRef, TagSelector } from '@/components';
 import type * as Type from '@/common/interface';
 import { DRAFT_QUESTION_STORAGE_KEY } from '@/common/constants';
@@ -42,6 +42,7 @@ import {
 } from '@/services';
 import { handleFormError, SaveDraft, storageExpires } from '@/utils';
 import { pathFactory } from '@/router/pathFactory';
+import { useCaptchaPlugin } from '@/utils/pluginKit';
 
 import SearchQuestion from './components/SearchQuestion';
 
@@ -120,8 +121,8 @@ const Ask = () => {
 
   const isEdit = qid !== undefined;
 
-  const saveCaptcha = useCaptchaModal('question');
-  const editCaptcha = useCaptchaModal('edit');
+  const saveCaptcha = useCaptchaPlugin('question');
+  const editCaptcha = useCaptchaPlugin('edit');
 
   const removeDraft = () => {
     saveDraft.save.cancel();
@@ -276,6 +277,79 @@ const Ask = () => {
     }
   };
 
+  const submitModifyQuestion = (params) => {
+    contentChangedRef.current = false;
+    const ep = {
+      ...params,
+      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;
+    }
+    modifyQuestion(ep)
+      .then(async (res) => {
+        await editCaptcha?.close();
+        navigate(pathFactory.questionLanding(qid, res?.url_title), {
+          state: { isReview: res?.wait_for_review },
+        });
+      })
+      .catch((err) => {
+        if (err.isError) {
+          editCaptcha?.handleCaptchaError(err.list);
+          const data = handleFormError(err, formData);
+          setFormData({ ...data });
+        }
+      });
+  };
+
+  const submitQuestion = async (params) => {
+    contentChangedRef.current = false;
+    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 saveQuestionWithAnswer({
+        ...params,
+        answer_content: formData.answer_content.value,
+      }).catch((err) => {
+        if (err.isError) {
+          const captchaErr = saveCaptcha?.handleCaptchaError(err.list);
+          if (!(captchaErr && err.list.length === 1)) {
+            const data = handleFormError(err, formData);
+            setFormData({ ...data });
+          }
+        }
+      });
+    } else {
+      res = await saveQuestion(params).catch((err) => {
+        if (err.isError) {
+          const captchaErr = saveCaptcha?.handleCaptchaError(err.list);
+          if (!(captchaErr && err.list.length === 1)) {
+            const data = handleFormError(err, formData);
+            setFormData({ ...data });
+          }
+        }
+      });
+    }
+
+    const id = res?.id || res?.question?.id;
+    if (id) {
+      await saveCaptcha?.close();
+      if (checked) {
+        navigate(pathFactory.questionLanding(id, res?.question?.url_title));
+      } else {
+        navigate(pathFactory.questionLanding(id, res?.url_title));
+      }
+    }
+    removeDraft();
+  };
+
   const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
     event.preventDefault();
     event.stopPropagation();
@@ -287,77 +361,20 @@ const Ask = () => {
     };
 
     if (isEdit) {
+      if (!editCaptcha) {
+        submitModifyQuestion(params);
+        return;
+      }
       editCaptcha.check(() => {
-        contentChangedRef.current = false;
-        const ep = {
-          ...params,
-          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;
-        }
-        modifyQuestion(ep)
-          .then(async (res) => {
-            await editCaptcha.close();
-            navigate(pathFactory.questionLanding(qid, res?.url_title), {
-              state: { isReview: res?.wait_for_review },
-            });
-          })
-          .catch((err) => {
-            if (err.isError) {
-              editCaptcha.handleCaptchaError(err.list);
-              const data = handleFormError(err, formData);
-              setFormData({ ...data });
-            }
-          });
+        submitModifyQuestion(params);
       });
     } else {
-      saveCaptcha.check(async () => {
-        contentChangedRef.current = false;
-        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 saveQuestionWithAnswer({
-            ...params,
-            answer_content: formData.answer_content.value,
-          }).catch((err) => {
-            if (err.isError) {
-              const captchaErr = saveCaptcha.handleCaptchaError(err.list);
-              if (!(captchaErr && err.list.length === 1)) {
-                const data = handleFormError(err, formData);
-                setFormData({ ...data });
-              }
-            }
-          });
-        } else {
-          res = await saveQuestion(params).catch((err) => {
-            if (err.isError) {
-              const captchaErr = saveCaptcha.handleCaptchaError(err.list);
-              if (!(captchaErr && err.list.length === 1)) {
-                const data = handleFormError(err, formData);
-                setFormData({ ...data });
-              }
-            }
-          });
-        }
-
-        const id = res?.id || res?.question?.id;
-        if (id) {
-          await saveCaptcha.close();
-          if (checked) {
-            navigate(pathFactory.questionLanding(id, res?.question?.url_title));
-          } else {
-            navigate(pathFactory.questionLanding(id, res?.url_title));
-          }
-        }
-        removeDraft();
+      if (!saveCaptcha) {
+        submitQuestion(params);
+        return;
+      }
+      saveCaptcha?.check(async () => {
+        submitQuestion(params);
       });
     }
   };
diff --git a/ui/src/pages/Questions/Detail/components/InviteToAnswer/index.tsx b/ui/src/pages/Questions/Detail/components/InviteToAnswer/index.tsx
index 82f9fa27..b8e29942 100644
--- a/ui/src/pages/Questions/Detail/components/InviteToAnswer/index.tsx
+++ b/ui/src/pages/Questions/Detail/components/InviteToAnswer/index.tsx
@@ -27,7 +27,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 { useCaptchaPlugin } from '@/utils/pluginKit';
 
 import PeopleDropdown from './PeopleDropdown';
 
@@ -44,7 +44,7 @@ const Index: FC<Props> = ({ questionId, readOnly = false }) => {
 
   const [editing, setEditing] = useState(false);
   const [users, setUsers] = useState<Type.UserInfoBase[]>([]);
-  const iaCaptcha = useCaptchaModal('invitation_answer');
+  const iaCaptcha = useCaptchaPlugin('invitation_answer');
 
   const initInviteUsers = () => {
     if (!questionId) {
@@ -74,28 +74,35 @@ const Index: FC<Props> = ({ questionId, readOnly = false }) => {
     setUsers(userList);
   };
 
+  const submitInviteUser = () => {
+    const names = users.map((_) => {
+      return _.username;
+    });
+    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);
+        }
+      });
+  };
+
   const saveInviteUsers = () => {
     if (!users) {
       return;
     }
-    const names = users.map((_) => {
-      return _.username;
-    });
-    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.error('putInviteUser error: ', ex);
-        });
-    });
+
+    if (!iaCaptcha) {
+      submitInviteUser();
+      return;
+    }
+
+    iaCaptcha.check(() => submitInviteUser());
   };
 
   useEffect(() => {
diff --git a/ui/src/pages/Questions/Detail/components/WriteAnswer/index.tsx b/ui/src/pages/Questions/Detail/components/WriteAnswer/index.tsx
index 24a301f8..456fbe37 100644
--- a/ui/src/pages/Questions/Detail/components/WriteAnswer/index.tsx
+++ b/ui/src/pages/Questions/Detail/components/WriteAnswer/index.tsx
@@ -25,7 +25,8 @@ import { Link } from 'react-router-dom';
 import { marked } from 'marked';
 import classNames from 'classnames';
 
-import { usePromptWithUnload, useCaptchaModal } from '@/hooks';
+import { usePromptWithUnload } from '@/hooks';
+import { useCaptchaPlugin } from '@/utils/pluginKit';
 import { Editor, Modal, TextArea } from '@/components';
 import { FormDataType, PostAnswerReq } from '@/common/interface';
 import { postAnswer } from '@/services';
@@ -63,7 +64,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');
+  const aCaptcha = useCaptchaPlugin('answer');
   const writeInfo = writeSettingStore((state) => state.write);
   const [editorCanSave, setEditorCanSave] = useState(false);
 
@@ -156,6 +157,40 @@ const Index: FC<Props> = ({ visible = false, data, callback }) => {
     }
   };
 
+  const submitAnswer = () => {
+    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 });
+        }
+      });
+  };
+
   const handleSubmit = () => {
     if (!guard.tryNormalLogged(true)) {
       return;
@@ -163,40 +198,11 @@ const Index: FC<Props> = ({ visible = false, data, callback }) => {
     if (!checkValidated()) {
       return;
     }
-
-    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 });
-          }
-        });
-    });
+    if (!aCaptcha) {
+      submitAnswer();
+      return;
+    }
+    aCaptcha.check(() => submitAnswer());
   };
 
   const clickBtn = () => {
diff --git a/ui/src/pages/Questions/EditAnswer/index.tsx b/ui/src/pages/Questions/EditAnswer/index.tsx
index fbb3e327..49e7b33a 100644
--- a/ui/src/pages/Questions/EditAnswer/index.tsx
+++ b/ui/src/pages/Questions/EditAnswer/index.tsx
@@ -26,7 +26,8 @@ import dayjs from 'dayjs';
 import classNames from 'classnames';
 
 import { handleFormError, scrollToDocTop } from '@/utils';
-import { usePageTags, usePromptWithUnload, useCaptchaModal } from '@/hooks';
+import { usePageTags, usePromptWithUnload } from '@/hooks';
+import { useCaptchaPlugin, useRenderHtmlPlugin } from '@/utils/pluginKit';
 import { pathFactory } from '@/router/pathFactory';
 import { Editor, EditorRef, Icon, htmlRender } from '@/components';
 import type * as Type from '@/common/interface';
@@ -35,7 +36,6 @@ import {
   modifyAnswer,
   useQueryRevisions,
 } from '@/services';
-import { useRenderHtmlPlugin } from '@/utils/pluginKit';
 
 import './index.scss';
 
@@ -71,7 +71,7 @@ const Index = () => {
   const [formData, setFormData] = useState<FormDataItem>(initFormData);
   const [immData, setImmData] = useState(initFormData);
   const [contentChanged, setContentChanged] = useState(false);
-  const editCaptcha = useCaptchaModal('edit');
+  const editCaptcha = useCaptchaPlugin('edit');
 
   useEffect(() => {
     if (data?.info?.content) {
@@ -153,6 +153,39 @@ const Index = () => {
     return bol;
   };
 
+  const submitEditAnswer = () => {
+    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 handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
     setContentChanged(false);
 
@@ -163,38 +196,11 @@ const Index = () => {
       return;
     }
 
-    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 });
-          }
-        });
-    });
+    if (!editCaptcha) {
+      submitEditAnswer();
+      return;
+    }
+    editCaptcha.check(() => submitEditAnswer());
   };
   const handleSelectedRevision = (e) => {
     const index = e.target.value;
diff --git a/ui/src/pages/Review/components/ApproveDropdown/index.tsx b/ui/src/pages/Review/components/ApproveDropdown/index.tsx
index f6a62471..b2c05c96 100644
--- a/ui/src/pages/Review/components/ApproveDropdown/index.tsx
+++ b/ui/src/pages/Review/components/ApproveDropdown/index.tsx
@@ -23,7 +23,8 @@ import { useTranslation } from 'react-i18next';
 
 import { Modal } from '@/components';
 import { putFlagReviewAction } from '@/services';
-import { useCaptchaModal, useReportModal, useToast } from '@/hooks';
+import { useReportModal, useToast } from '@/hooks';
+import { useCaptchaPlugin } from '@/utils/pluginKit';
 import type * as Type from '@/common/interface';
 import EditPostModal from '../EditPostModal';
 
@@ -46,12 +47,52 @@ const Index: FC<IProps> = ({
   const [showEditPostModal, setShowEditPostModal] = useState(false);
   const closeModal = useReportModal(approveCallback);
   const toast = useToast();
-  const dCaptcha = useCaptchaModal('delete');
+  const dCaptcha = useCaptchaPlugin('delete');
 
   const handleEditPostModalState = () => {
     setShowEditPostModal(!showEditPostModal);
   };
 
+  const submitReviewAction = () => {
+    const req: Type.PutFlagReviewParams = {
+      operation_type: 'delete_post',
+      flag_id: String(itemData?.flag_id),
+      captcha_code: undefined,
+      captcha_id: undefined,
+    };
+    dCaptcha?.resolveCaptchaReq(req);
+
+    delete req.captcha_code;
+    delete req.captcha_id;
+
+    putFlagReviewAction(req)
+      .then(async () => {
+        await dCaptcha?.close();
+        let msg = '';
+        if (objectType === 'question') {
+          msg = t('post_deleted', { keyPrefix: 'messages' });
+        }
+        if (objectType === 'answer') {
+          msg = t('tip_answer_deleted');
+        }
+        if (objectType === 'answer' || objectType === 'question') {
+          toast.onShow({
+            msg,
+            variant: 'success',
+          });
+        }
+        approveCallback();
+      })
+      .catch((ex) => {
+        if (ex.isError) {
+          dCaptcha?.handleCaptchaError(ex.list);
+        }
+      })
+      .finally(() => {
+        setIsLoading(false);
+      });
+  };
+
   const handleDelete = () => {
     let content = '';
 
@@ -78,45 +119,11 @@ const Index: FC<IProps> = ({
       confirmBtnVariant: 'danger',
       confirmText: t('delete', { keyPrefix: 'btns' }),
       onConfirm: () => {
-        dCaptcha.check(() => {
-          const req: Type.PutFlagReviewParams = {
-            operation_type: 'delete_post',
-            flag_id: String(itemData?.flag_id),
-            captcha_code: undefined,
-            captcha_id: undefined,
-          };
-          dCaptcha.resolveCaptchaReq(req);
-
-          delete req.captcha_code;
-          delete req.captcha_id;
-
-          putFlagReviewAction(req)
-            .then(async () => {
-              await dCaptcha.close();
-              let msg = '';
-              if (objectType === 'question') {
-                msg = t('post_deleted', { keyPrefix: 'messages' });
-              }
-              if (objectType === 'answer') {
-                msg = t('tip_answer_deleted');
-              }
-              if (objectType === 'answer' || objectType === 'question') {
-                toast.onShow({
-                  msg,
-                  variant: 'success',
-                });
-              }
-              approveCallback();
-            })
-            .catch((ex) => {
-              if (ex.isError) {
-                dCaptcha.handleCaptchaError(ex.list);
-              }
-            })
-            .finally(() => {
-              setIsLoading(false);
-            });
-        });
+        if (!dCaptcha) {
+          submitReviewAction();
+          return;
+        }
+        dCaptcha.check(() => submitReviewAction());
       },
       onCancel: () => {
         setIsLoading(false);
diff --git a/ui/src/pages/Review/components/EditPostModal/index.tsx b/ui/src/pages/Review/components/EditPostModal/index.tsx
index 0eefd311..4ee80b0a 100644
--- a/ui/src/pages/Review/components/EditPostModal/index.tsx
+++ b/ui/src/pages/Review/components/EditPostModal/index.tsx
@@ -24,7 +24,8 @@ import { useTranslation } from 'react-i18next';
 import classNames from 'classnames';
 
 import { putFlagReviewAction } from '@/services';
-import { useCaptchaModal, usePageUsers } from '@/hooks';
+import { usePageUsers } from '@/hooks';
+import { useCaptchaPlugin } from '@/utils/pluginKit';
 import { Editor, TagSelector, Mentions, TextArea } from '@/components';
 import {
   // matchedUsers,
@@ -89,7 +90,7 @@ const Index: FC<Props> = ({
   const [loaded, setLoaded] = useState(false);
   const pageUsers = usePageUsers();
 
-  const editCaptcha = useCaptchaModal('edit');
+  const editCaptcha = useCaptchaPlugin('edit');
 
   const onClose = (bol) => {
     if (bol) {
@@ -160,14 +161,7 @@ const Index: FC<Props> = ({
     return bol;
   };
 
-  const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
-    event.preventDefault();
-    event.stopPropagation();
-
-    if (!checkValidated()) {
-      return;
-    }
-
+  const submitFlagReviewAction = () => {
     const params: Type.PutFlagReviewParams = {
       title: formData.title.value,
       content: formData.content.value,
@@ -191,28 +185,41 @@ const Index: FC<Props> = ({
       delete params.title;
       delete params.tags;
     }
-
-    editCaptcha.check(() => {
-      if (objectType === 'question') {
-        const imgCode = editCaptcha.getCaptcha();
-        if (imgCode.verify) {
-          params.captcha_code = imgCode.captcha_code;
-          params.captcha_id = imgCode.captcha_id;
-        }
+    if (objectType === 'question') {
+      const imgCode = editCaptcha?.getCaptcha();
+      if (imgCode?.verify) {
+        params.captcha_code = imgCode.captcha_code;
+        params.captcha_id = imgCode.captcha_id;
       }
-      putFlagReviewAction(params)
-        .then(async () => {
-          await editCaptcha.close();
-          onClose(true);
-        })
-        .catch((err) => {
-          if (err.isError) {
-            editCaptcha.handleCaptchaError(err.list);
-            const data = handleFormError(err, formData);
-            setFormData({ ...data });
-          }
-        });
-    });
+    }
+    putFlagReviewAction(params)
+      .then(async () => {
+        await editCaptcha?.close();
+        onClose(true);
+      })
+      .catch((err) => {
+        if (err.isError) {
+          editCaptcha?.handleCaptchaError(err.list);
+          const data = handleFormError(err, formData);
+          setFormData({ ...data });
+        }
+      });
+  };
+
+  const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
+    event.preventDefault();
+    event.stopPropagation();
+
+    if (!checkValidated()) {
+      return;
+    }
+
+    if (!editCaptcha) {
+      submitFlagReviewAction();
+      return;
+    }
+
+    editCaptcha.check(() => submitFlagReviewAction());
   };
 
   const handleSelected = (val) => {
diff --git a/ui/src/pages/Search/index.tsx b/ui/src/pages/Search/index.tsx
index 8517172d..a016af70 100644
--- a/ui/src/pages/Search/index.tsx
+++ b/ui/src/pages/Search/index.tsx
@@ -22,7 +22,8 @@ import { useTranslation } from 'react-i18next';
 import { useSearchParams } from 'react-router-dom';
 import { useEffect, useState } from 'react';
 
-import { usePageTags, useCaptchaModal, useSkeletonControl } from '@/hooks';
+import { usePageTags, useSkeletonControl } from '@/hooks';
+import { useCaptchaPlugin } from '@/utils/pluginKit';
 import { Pagination } from '@/components';
 import { getSearchResult } from '@/services';
 import type { SearchParams, SearchRes } from '@/common/interface';
@@ -51,7 +52,7 @@ const Index = () => {
   });
   const { count = 0, list = [], extra = null } = data || {};
 
-  const searchCaptcha = useCaptchaModal('search');
+  const searchCaptcha = useCaptchaPlugin('search');
 
   const doSearch = () => {
     setIsLoading(true);
@@ -62,7 +63,7 @@ const Index = () => {
       size: 20,
     };
 
-    const captcha = searchCaptcha.getCaptcha();
+    const captcha = searchCaptcha?.getCaptcha();
     if (captcha?.verify) {
       params.captcha_id = captcha.captcha_id;
       params.captcha_code = captcha.captcha_code;
@@ -70,12 +71,12 @@ const Index = () => {
 
     getSearchResult(params)
       .then(async (resp) => {
-        await searchCaptcha.close();
+        await searchCaptcha?.close();
         setData(resp);
       })
       .catch((err) => {
         if (err.isError) {
-          searchCaptcha.handleCaptchaError(err.list);
+          searchCaptcha?.handleCaptchaError(err.list);
         }
       })
       .finally(() => {
@@ -84,6 +85,10 @@ const Index = () => {
   };
 
   useEffect(() => {
+    if (!searchCaptcha) {
+      doSearch();
+      return;
+    }
     searchCaptcha.check(() => {
       doSearch();
     });
diff --git a/ui/src/pages/Users/AccountForgot/components/sendEmail.tsx b/ui/src/pages/Users/AccountForgot/components/sendEmail.tsx
index acca34aa..5d7d2c9b 100644
--- a/ui/src/pages/Users/AccountForgot/components/sendEmail.tsx
+++ b/ui/src/pages/Users/AccountForgot/components/sendEmail.tsx
@@ -24,7 +24,7 @@ import { useTranslation } from 'react-i18next';
 import type { PasswordResetReq, FormDataType } from '@/common/interface';
 import { resetPassword } from '@/services';
 import { handleFormError } from '@/utils';
-import { useCaptchaModal } from '@/hooks';
+import { useCaptchaPlugin } from '@/utils/pluginKit';
 
 interface IProps {
   // eslint-disable-next-line react/no-unused-prop-types
@@ -42,7 +42,7 @@ const Index: FC<IProps> = ({ callback }) => {
     },
   });
 
-  const emailCaptcha = useCaptchaModal('email');
+  const emailCaptcha = useCaptchaPlugin('email');
 
   const handleChange = (params: FormDataType) => {
     setFormData({ ...formData, ...params });
@@ -73,20 +73,20 @@ const Index: FC<IProps> = ({ callback }) => {
       e_mail: formData.e_mail.value,
     };
 
-    const captcha = emailCaptcha.getCaptcha();
-    if (captcha.verify) {
+    const captcha = emailCaptcha?.getCaptcha();
+    if (captcha?.verify) {
       params.captcha_code = captcha.captcha_code;
       params.captcha_id = captcha.captcha_id;
     }
 
     resetPassword(params)
       .then(async () => {
-        await emailCaptcha.close();
+        await emailCaptcha?.close();
         callback?.(2, formData.e_mail.value);
       })
       .catch((err) => {
         if (err.isError) {
-          emailCaptcha.handleCaptchaError(err.list);
+          emailCaptcha?.handleCaptchaError(err.list);
           const data = handleFormError(err, formData);
           setFormData({ ...data });
         }
@@ -101,6 +101,11 @@ const Index: FC<IProps> = ({ callback }) => {
       return;
     }
 
+    if (!emailCaptcha) {
+      sendEmail();
+      return;
+    }
+
     emailCaptcha.check(() => {
       sendEmail();
     });
diff --git a/ui/src/pages/Users/ChangeEmail/components/sendEmail.tsx b/ui/src/pages/Users/ChangeEmail/components/sendEmail.tsx
index cf321a1f..419e226e 100644
--- a/ui/src/pages/Users/ChangeEmail/components/sendEmail.tsx
+++ b/ui/src/pages/Users/ChangeEmail/components/sendEmail.tsx
@@ -26,7 +26,7 @@ import type { PasswordResetReq, FormDataType } from '@/common/interface';
 import { loggedUserInfoStore } from '@/stores';
 import { changeEmail } from '@/services';
 import { handleFormError } from '@/utils';
-import { useCaptchaModal } from '@/hooks';
+import { useCaptchaPlugin } from '@/utils/pluginKit';
 
 const Index: FC = () => {
   const { t } = useTranslation('translation', { keyPrefix: 'change_email' });
@@ -41,7 +41,7 @@ const Index: FC = () => {
   const navigate = useNavigate();
   const { user: userInfo, update: updateUser } = loggedUserInfoStore();
 
-  const emailCaptcha = useCaptchaModal('email');
+  const emailCaptcha = useCaptchaPlugin('email');
 
   const handleChange = (params: FormDataType) => {
     setFormData({ ...formData, ...params });
@@ -71,22 +71,22 @@ const Index: FC = () => {
     const params: PasswordResetReq = {
       e_mail: formData.e_mail.value,
     };
-    const imgCode = emailCaptcha.getCaptcha();
-    if (imgCode.verify) {
+    const imgCode = emailCaptcha?.getCaptcha();
+    if (imgCode?.verify) {
       params.captcha_code = imgCode.captcha_code;
       params.captcha_id = imgCode.captcha_id;
     }
 
     changeEmail(params)
       .then(async () => {
-        await emailCaptcha.close();
+        await emailCaptcha?.close();
         userInfo.e_mail = formData.e_mail.value;
         updateUser(userInfo);
         navigate('/users/login', { replace: true });
       })
       .catch((err) => {
         if (err.isError) {
-          emailCaptcha.handleCaptchaError(err.list);
+          emailCaptcha?.handleCaptchaError(err.list);
           const data = handleFormError(err, formData);
           setFormData({ ...data });
         }
@@ -99,7 +99,10 @@ const Index: FC = () => {
     if (!checkValidated()) {
       return;
     }
-
+    if (!emailCaptcha) {
+      sendEmail();
+      return;
+    }
     emailCaptcha.check(() => {
       sendEmail();
     });
diff --git a/ui/src/pages/Users/Login/index.tsx b/ui/src/pages/Users/Login/index.tsx
index bbb1f67f..52bbd00e 100644
--- a/ui/src/pages/Users/Login/index.tsx
+++ b/ui/src/pages/Users/Login/index.tsx
@@ -22,7 +22,7 @@ import { Container, Form, Button, Col } from 'react-bootstrap';
 import { Link, useNavigate, useSearchParams } from 'react-router-dom';
 import { Trans, useTranslation } from 'react-i18next';
 
-import { usePageTags, useCaptchaModal } from '@/hooks';
+import { usePageTags } from '@/hooks';
 import type { LoginReqParams, FormDataType } from '@/common/interface';
 import { Unactivate, WelcomeTitle, PluginRender } from '@/components';
 import {
@@ -31,6 +31,7 @@ import {
   userCenterStore,
 } from '@/stores';
 import { floppyNavigation, guard, handleFormError, userCenter } from '@/utils';
+import { useCaptchaPlugin } from '@/utils/pluginKit';
 import { login, UcAgent } from '@/services';
 import { setupAppTheme } from '@/utils/localize';
 
@@ -68,7 +69,7 @@ const Index: React.FC = () => {
     setFormData({ ...formData, ...params });
   };
 
-  const passwordCaptcha = useCaptchaModal('password');
+  const passwordCaptcha = useCaptchaPlugin('password');
 
   const checkValidated = (): boolean => {
     let bol = true;
@@ -107,7 +108,7 @@ const Index: React.FC = () => {
       pass: formData.pass.value,
     };
 
-    const captcha = passwordCaptcha.getCaptcha();
+    const captcha = passwordCaptcha?.getCaptcha();
     if (captcha?.verify) {
       params.captcha_code = captcha.captcha_code;
       params.captcha_id = captcha.captcha_id;
@@ -115,7 +116,7 @@ const Index: React.FC = () => {
 
     login(params)
       .then(async (res) => {
-        await passwordCaptcha.close();
+        await passwordCaptcha?.close?.();
         updateUser(res);
         setupAppTheme();
         const userStat = guard.deriveLoginState();
@@ -130,7 +131,7 @@ const Index: React.FC = () => {
         if (err.isError) {
           const data = handleFormError(err, formData);
           setFormData({ ...data });
-          passwordCaptcha.handleCaptchaError(err.list);
+          passwordCaptcha?.handleCaptchaError?.(err.list);
         }
       });
   };
@@ -143,7 +144,12 @@ const Index: React.FC = () => {
       return;
     }
 
-    passwordCaptcha.check(() => {
+    if (!passwordCaptcha) {
+      handleLogin();
+      return;
+    }
+
+    passwordCaptcha?.check?.(() => {
       handleLogin();
     });
   };
@@ -165,6 +171,17 @@ const Index: React.FC = () => {
       <WelcomeTitle />
       {step === 1 ? (
         <Col className="mx-auto" md={6} lg={4} xl={3}>
+          <PluginRender
+            type="captcha"
+            slug_name="captcha_basic"
+            className="mb-5"
+          />
+
+          <PluginRender
+            type="captcha"
+            slug_name="captcha_google_v2"
+            className="mb-5"
+          />
           {ucAgentInfo ? (
             <PluginRender
               type="connector"
diff --git a/ui/src/pages/Users/Register/components/SignUpForm/index.tsx b/ui/src/pages/Users/Register/components/SignUpForm/index.tsx
index 0a04c550..433f0427 100644
--- a/ui/src/pages/Users/Register/components/SignUpForm/index.tsx
+++ b/ui/src/pages/Users/Register/components/SignUpForm/index.tsx
@@ -22,7 +22,7 @@ import { Form, Button } from 'react-bootstrap';
 import { Link } from 'react-router-dom';
 import { Trans, useTranslation } from 'react-i18next';
 
-import { useCaptchaModal } from '@/hooks';
+import { useCaptchaPlugin } from '@/utils/pluginKit';
 import type { FormDataType, RegisterReqParams } from '@/common/interface';
 import { register } from '@/services';
 import userStore from '@/stores/loggedUserInfo';
@@ -54,7 +54,7 @@ const Index: React.FC<Props> = ({ callback }) => {
   });
 
   const updateUser = userStore((state) => state.update);
-  const emailCaptcha = useCaptchaModal('email');
+  const emailCaptcha = useCaptchaPlugin('email');
 
   const handleChange = (params: FormDataType) => {
     setFormData({ ...formData, ...params });
@@ -99,7 +99,7 @@ const Index: React.FC<Props> = ({ callback }) => {
       pass: formData.pass.value,
     };
 
-    const captcha = emailCaptcha.getCaptcha();
+    const captcha = emailCaptcha?.getCaptcha();
     if (captcha?.verify) {
       reqParams.captcha_code = captcha.captcha_code;
       reqParams.captcha_id = captcha.captcha_id;
@@ -107,13 +107,13 @@ const Index: React.FC<Props> = ({ callback }) => {
 
     register(reqParams)
       .then(async (res) => {
-        await emailCaptcha.close();
+        await emailCaptcha?.close();
         updateUser(res);
         callback();
       })
       .catch((err) => {
         if (err.isError) {
-          emailCaptcha.handleCaptchaError(err.list);
+          emailCaptcha?.handleCaptchaError(err.list);
           const data = handleFormError(err, formData);
           setFormData({ ...data });
         }
@@ -126,6 +126,10 @@ const Index: React.FC<Props> = ({ callback }) => {
     if (!checkValidated()) {
       return;
     }
+    if (!emailCaptcha) {
+      handleRegister();
+      return;
+    }
     emailCaptcha.check(() => {
       handleRegister();
     });
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 ac42f078..27eda181 100644
--- a/ui/src/pages/Users/Settings/Account/components/ModifyEmail/index.tsx
+++ b/ui/src/pages/Users/Settings/Account/components/ModifyEmail/index.tsx
@@ -22,7 +22,8 @@ import { Form, Button } from 'react-bootstrap';
 import { useTranslation } from 'react-i18next';
 
 import type * as Type from '@/common/interface';
-import { useToast, useCaptchaModal } from '@/hooks';
+import { useToast } from '@/hooks';
+import { useCaptchaPlugin } from '@/utils/pluginKit';
 import { getLoggedUserInfo, changeEmail } from '@/services';
 import { handleFormError } from '@/utils';
 
@@ -45,7 +46,7 @@ const Index: FC = () => {
   });
   const [userInfo, setUserInfo] = useState<Type.UserInfoRes>();
   const toast = useToast();
-  const emailCaptcha = useCaptchaModal('edit_userinfo');
+  const emailCaptcha = useCaptchaPlugin('edit_userinfo');
 
   useEffect(() => {
     getLoggedUserInfo().then((resp) => {
@@ -108,14 +109,14 @@ const Index: FC = () => {
       pass: formData.pass.value,
     };
 
-    const imgCode = emailCaptcha.getCaptcha();
-    if (imgCode.verify) {
+    const imgCode = emailCaptcha?.getCaptcha();
+    if (imgCode?.verify) {
       params.captcha_code = imgCode.captcha_code;
       params.captcha_id = imgCode.captcha_id;
     }
     changeEmail(params)
       .then(async () => {
-        await emailCaptcha.close();
+        await emailCaptcha?.close();
         setStep(1);
         toast.onShow({
           msg: t('change_email_info'),
@@ -125,7 +126,7 @@ const Index: FC = () => {
       })
       .catch((err) => {
         if (err.isError) {
-          emailCaptcha.handleCaptchaError(err.list);
+          emailCaptcha?.handleCaptchaError(err.list);
           const data = handleFormError(err, formData);
           setFormData({ ...data });
         }
@@ -138,7 +139,10 @@ const Index: FC = () => {
     if (!checkValidated()) {
       return;
     }
-
+    if (!emailCaptcha) {
+      postEmail();
+      return;
+    }
     emailCaptcha.check(() => {
       postEmail();
     });
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 6b919c5c..bdc84bd7 100644
--- a/ui/src/pages/Users/Settings/Account/components/ModifyPass/index.tsx
+++ b/ui/src/pages/Users/Settings/Account/components/ModifyPass/index.tsx
@@ -23,7 +23,8 @@ import { useTranslation } from 'react-i18next';
 
 import classname from 'classnames';
 
-import { useToast, useCaptchaModal } from '@/hooks';
+import { useToast } from '@/hooks';
+import { useCaptchaPlugin } from '@/utils/pluginKit';
 import type { FormDataType } from '@/common/interface';
 import { modifyPassword } from '@/services';
 import { handleFormError } from '@/utils';
@@ -54,7 +55,7 @@ const Index: FC = () => {
     },
   });
 
-  const infoCaptcha = useCaptchaModal('edit_userinfo');
+  const infoCaptcha = useCaptchaPlugin('edit_userinfo');
 
   const handleFormState = () => {
     setFormState((pre) => !pre);
@@ -134,14 +135,14 @@ const Index: FC = () => {
       pass: formData.pass.value,
     };
 
-    const imgCode = infoCaptcha.getCaptcha();
-    if (imgCode.verify) {
+    const imgCode = infoCaptcha?.getCaptcha();
+    if (imgCode?.verify) {
       params.captcha_code = imgCode.captcha_code;
       params.captcha_id = imgCode.captcha_id;
     }
     modifyPassword(params)
       .then(async () => {
-        await infoCaptcha.close();
+        await infoCaptcha?.close();
         toast.onShow({
           msg: t('update_password', { keyPrefix: 'toast' }),
           variant: 'success',
@@ -150,7 +151,7 @@ const Index: FC = () => {
       })
       .catch((err) => {
         if (err.isError) {
-          infoCaptcha.handleCaptchaError(err.list);
+          infoCaptcha?.handleCaptchaError(err.list);
           const data = handleFormError(err, formData);
           setFormData({ ...data });
         }
@@ -163,6 +164,10 @@ const Index: FC = () => {
     if (!checkValidated()) {
       return;
     }
+    if (!infoCaptcha) {
+      postModifyPass();
+      return;
+    }
 
     infoCaptcha.check(() => {
       postModifyPass();
diff --git a/ui/src/plugins/builtin/HostingConnector/index.tsx b/ui/src/plugins/builtin/HostingConnector/index.tsx
index bcddf362..ae098637 100644
--- a/ui/src/plugins/builtin/HostingConnector/index.tsx
+++ b/ui/src/plugins/builtin/HostingConnector/index.tsx
@@ -23,8 +23,11 @@ import { useTranslation } from 'react-i18next';
 
 import classnames from 'classnames';
 
-import { PluginInfo } from '@/utils/pluginKit';
-import { getTransNs, getTransKeyPrefix } from '@/utils/pluginKit/utils';
+import {
+  getTransNs,
+  getTransKeyPrefix,
+  PluginInfo,
+} from '@/utils/pluginKit/utils';
 import { SvgIcon } from '@/components';
 import { userCenterStore } from '@/stores';
 import './i18n';
diff --git a/ui/src/plugins/builtin/SearchInfo/index.tsx b/ui/src/plugins/builtin/SearchInfo/index.tsx
index e5781975..24db0167 100644
--- a/ui/src/plugins/builtin/SearchInfo/index.tsx
+++ b/ui/src/plugins/builtin/SearchInfo/index.tsx
@@ -20,8 +20,11 @@
 import { memo, FC } from 'react';
 import { useTranslation } from 'react-i18next';
 
-import { PluginInfo } from '@/utils/pluginKit';
-import { getTransNs, getTransKeyPrefix } from '@/utils/pluginKit/utils';
+import {
+  getTransNs,
+  getTransKeyPrefix,
+  PluginInfo,
+} from '@/utils/pluginKit/utils';
 import { SvgIcon } from '@/components';
 
 import info from './info.yaml';
diff --git a/ui/src/plugins/builtin/ThirdPartyConnector/index.tsx b/ui/src/plugins/builtin/ThirdPartyConnector/index.tsx
index 99032b20..14ba7c0c 100644
--- a/ui/src/plugins/builtin/ThirdPartyConnector/index.tsx
+++ b/ui/src/plugins/builtin/ThirdPartyConnector/index.tsx
@@ -23,8 +23,11 @@ import { useTranslation } from 'react-i18next';
 
 import classnames from 'classnames';
 
-import { PluginInfo } from '@/utils/pluginKit';
-import { getTransNs, getTransKeyPrefix } from '@/utils/pluginKit/utils';
+import {
+  getTransNs,
+  getTransKeyPrefix,
+  PluginInfo,
+} from '@/utils/pluginKit/utils';
 import { SvgIcon } from '@/components';
 
 import info from './info.yaml';
diff --git a/ui/src/router/pathFactory.ts b/ui/src/router/pathFactory.ts
index ea353604..256dcbf0 100644
--- a/ui/src/router/pathFactory.ts
+++ b/ui/src/router/pathFactory.ts
@@ -34,7 +34,7 @@ const tagEdit = (tagId: string) => {
   return r;
 };
 
-const questionLanding = (questionId: string, slugTitle: string = '') => {
+const questionLanding = (questionId: string = '', slugTitle: string = '') => {
   const { seo } = seoSettingStore.getState();
   if (!questionId) {
     return slugTitle ? `/questions/null/${slugTitle}` : '/questions/null';
diff --git a/ui/src/utils/pluginKit/index.ts b/ui/src/utils/pluginKit/index.ts
index 481fadd3..eca058bd 100644
--- a/ui/src/utils/pluginKit/index.ts
+++ b/ui/src/utils/pluginKit/index.ts
@@ -121,6 +121,11 @@ class Plugins {
     return this.plugins.find((p) => p.info.slug_name === slug_name);
   }
 
+  getOnePluginHooks(slug_name: string) {
+    const plugin = this.getPlugin(slug_name);
+    return plugin?.hooks;
+  }
+
   getPlugins() {
     return this.plugins;
   }
@@ -179,6 +184,16 @@ const mergeRoutePlugins = (routes) => {
   return routes;
 };
 
-export { useRenderHtmlPlugin, mergeRoutePlugins };
+// Only one captcha type plug-in can be enabled at the same time
+const useCaptchaPlugin = (key: Type.CaptchaKey) => {
+  const captcha = plugins
+    .getPlugins()
+    .filter((plugin) => plugin.info.type === 'captcha');
+  const pluginHooks = plugins.getOnePluginHooks(captcha[0]?.info.slug_name);
+  return pluginHooks?.useCaptcha?.(key);
+};
+
 export type { Plugin, PluginInfo, PluginType };
+
+export { useRenderHtmlPlugin, mergeRoutePlugins, useCaptchaPlugin };
 export default plugins;
diff --git a/ui/src/utils/pluginKit/utils.ts b/ui/src/utils/pluginKit/utils.ts
index a179360d..a6ca44ce 100644
--- a/ui/src/utils/pluginKit/utils.ts
+++ b/ui/src/utils/pluginKit/utils.ts
@@ -21,6 +21,8 @@ import { NamedExoticComponent, FC } from 'react';
 
 import i18next from 'i18next';
 
+import type * as Type from '@/common/interface';
+
 /**
  * This information is to be defined for all components.
  * It may be used for feature upgrades or version compatibility processing.
@@ -35,7 +37,12 @@ import i18next from 'i18next';
 
 const I18N_NS = 'plugin';
 
-export type PluginType = 'connector' | 'search' | 'editor' | 'route';
+export type PluginType =
+  | 'connector'
+  | 'search'
+  | 'editor'
+  | 'route'
+  | 'captcha';
 export interface PluginInfo {
   slug_name: string;
   type: PluginType;
@@ -50,6 +57,13 @@ export interface Plugin {
   i18nConfig?;
   hooks?: {
     useRender?: Array<(element: HTMLElement | null) => void>;
+    useCaptcha?: (key: Type.CaptchaKey) => {
+      getCaptcha: () => Record<string, any>;
+      check: (t: () => void) => void;
+      handleCaptchaError: (error) => any;
+      close: () => Promise<void>;
+      resolveCaptchaReq: (data) => void;
+    };
   };
   activated?: boolean;
 }