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

shuai pushed a commit to branch test
in repository https://gitbox.apache.org/repos/asf/answer.git


The following commit(s) were added to refs/heads/test by this push:
     new 630ac20a Management Backend Menu and Function Adjustments (#1474)
630ac20a is described below

commit 630ac20a384cad4b4e1527528c2fbbc50b937933
Author: dashuai <[email protected]>
AuthorDate: Thu Jan 22 14:07:30 2026 +0800

    Management Backend Menu and Function Adjustments (#1474)
    
    …ent, regardless of functional split or reorganization
---
 i18n/en_US.yaml                                    |   9 +-
 i18n/zh_CN.yaml                                    |   9 +-
 ui/src/common/constants.ts                         |  58 ++-
 ui/src/common/interface.ts                         |  37 +-
 ui/src/components/AccordionNav/index.tsx           |  65 ++-
 ui/src/components/AdminSideNav/index.tsx           |  13 +-
 ui/src/components/SchemaForm/components/Switch.tsx |   3 +-
 ui/src/components/SchemaForm/index.tsx             |   8 +-
 ui/src/components/SchemaForm/types.ts              |   2 +-
 ui/src/components/TabNav/index.tsx                 |  26 ++
 ui/src/components/index.ts                         |   2 +
 ui/src/pages/Admin/Answers/index.tsx               |   8 +-
 ui/src/pages/Admin/Branding/index.tsx              |  16 +-
 ui/src/pages/Admin/CssAndHtml/index.tsx            |  16 +-
 .../Dashboard/components/HealthStatus/index.tsx    |   6 +-
 ui/src/pages/Admin/Files/index.tsx                 | 261 ++++++++++++
 ui/src/pages/Admin/General/index.tsx               |  29 +-
 ui/src/pages/Admin/Interface/index.tsx             |  75 +---
 ui/src/pages/Admin/Login/index.tsx                 |  30 +-
 ui/src/pages/Admin/Plugins/Config/index.tsx        |  18 +-
 ui/src/pages/Admin/{Legal => Policies}/index.tsx   |  63 +--
 ui/src/pages/Admin/Privileges/index.tsx            |  32 +-
 ui/src/pages/Admin/QaSettings/index.tsx            | 132 ++++++
 ui/src/pages/Admin/Questions/index.tsx             |   4 +-
 ui/src/pages/Admin/Security/index.tsx              | 145 +++++++
 ui/src/pages/Admin/Seo/index.tsx                   |  16 +-
 ui/src/pages/Admin/Smtp/index.tsx                  |  16 +-
 ui/src/pages/Admin/TagsSettings/index.tsx          | 170 ++++++++
 ui/src/pages/Admin/Themes/index.tsx                |  16 +-
 ui/src/pages/Admin/Users/index.tsx                 |   3 +
 ui/src/pages/Admin/UsersSettings/index.tsx         | 132 ++++++
 ui/src/pages/Admin/Write/index.tsx                 | 473 ---------------------
 ui/src/pages/Admin/index.scss                      |   4 +
 ui/src/pages/Admin/index.tsx                       |  16 +-
 ui/src/pages/Layout/index.tsx                      |   5 +-
 ui/src/pages/Users/Settings/Profile/index.tsx      |  10 +-
 ui/src/router/routes.ts                            |  36 +-
 ui/src/services/admin/index.ts                     |   1 +
 ui/src/services/admin/question.ts                  |  10 +
 ui/src/services/admin/settings.ts                  |  38 +-
 .../Admin/index.scss => services/admin/tags.ts}    |  23 +-
 ui/src/services/admin/users.ts                     |  19 +
 ui/src/stores/index.ts                             |   4 +-
 ui/src/stores/interface.ts                         |   4 +-
 ui/src/stores/loginSetting.ts                      |   1 -
 ui/src/stores/siteInfo.ts                          |   3 +-
 ui/src/stores/{siteLegal.ts => siteSecurity.ts}    |  16 +-
 ui/src/stores/writeSetting.ts                      |  12 +-
 ui/src/utils/guard.ts                              |  15 +-
 49 files changed, 1304 insertions(+), 806 deletions(-)

diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml
index 653793f2..ea607366 100644
--- a/i18n/en_US.yaml
+++ b/i18n/en_US.yaml
@@ -1816,6 +1816,13 @@ ui:
     plugins: Plugins
     installed_plugins: Installed Plugins
     apperance: Appearance
+    community: Community
+    advanced: Advanced
+    tags: Tags
+    rules: Rules
+    policies: Policies
+    security: Security
+    files: Files
   website_welcome: Welcome to {{site_name}}
   user_center:
     login: Login
@@ -2128,7 +2135,7 @@ ui:
         always_display: Always display external content
         ask_before_display: Ask before displaying external content
     write:
-      page_title: Write
+      page_title: Files
       min_content:
         label: Minimum question body length
         text: Minimum allowed question body length in characters.
diff --git a/i18n/zh_CN.yaml b/i18n/zh_CN.yaml
index a6c7f198..29e18151 100644
--- a/i18n/zh_CN.yaml
+++ b/i18n/zh_CN.yaml
@@ -1778,6 +1778,13 @@ ui:
     plugins: 插件
     installed_plugins: 已安装插件
     apperance: 外观
+    community: 社区
+    advanced: 高级
+    tags: 标签
+    rules: 规则
+    policies: 政策
+    security: 安全
+    files: 文件
   website_welcome: 欢迎来到 {{site_name}}
   user_center:
     login: 登录
@@ -2089,7 +2096,7 @@ ui:
         always_display: 总是显示外部内容
         ask_before_display: 在显示外部内容之前询问
     write:
-      page_title: 编辑
+      page_title: 文件
       min_content:
         label: 最小问题长度
         text: 最小允许的问题内容长度(字符)。
diff --git a/ui/src/common/constants.ts b/ui/src/common/constants.ts
index 18f25114..285da474 100644
--- a/ui/src/common/constants.ts
+++ b/ui/src/common/constants.ts
@@ -83,6 +83,11 @@ export const ADMIN_LIST_STATUS = {
   },
 };
 
+/**
+ * ADMIN_NAV_MENUS is the navigation menu for the admin panel.
+ * pathPrefix is used to activate the menu item when the activeKey starts with 
the pathPrefix.
+ */
+
 export const ADMIN_NAV_MENUS = [
   {
     name: 'dashboard',
@@ -92,15 +97,19 @@ export const ADMIN_NAV_MENUS = [
   {
     name: 'contents',
     icon: 'file-earmark-text-fill',
-    children: [{ name: 'questions' }, { name: 'answers' }],
+    children: [
+      { name: 'questions', path: 'qa/questions', pathPrefix: 'qa/' },
+      { name: 'tags', path: 'tags/settings', pathPrefix: 'tags/' },
+    ],
   },
   {
-    name: 'users',
+    name: 'community',
     icon: 'people-fill',
-  },
-  {
-    name: 'badges',
-    icon: 'award-fill',
+    children: [
+      { name: 'users', pathPrefix: 'users/' },
+      { name: 'badges' },
+      { name: 'rules', path: 'rules/privileges', pathPrefix: 'rules/' },
+    ],
   },
   {
     name: 'apperance',
@@ -113,20 +122,19 @@ export const ADMIN_NAV_MENUS = [
         name: 'customize',
       },
       { name: 'branding' },
+      { name: 'interface' },
     ],
   },
   {
-    name: 'settings',
+    name: 'advanced',
     icon: 'gear-fill',
     children: [
       { name: 'general' },
-      { name: 'interface' },
-      { name: 'smtp' },
-      { name: 'legal' },
-      { name: 'write' },
-      { name: 'seo' },
+      { name: 'security' },
+      { name: 'files' },
       { name: 'login' },
-      { name: 'privileges' },
+      { name: 'seo' },
+      { name: 'smtp' },
     ],
   },
   {
@@ -141,6 +149,30 @@ export const ADMIN_NAV_MENUS = [
   },
 ];
 
+export const ADMIN_QA_NAV_MENUS = [
+  { name: 'questions', path: '/admin/qa/questions' },
+  { name: 'answers', path: '/admin/qa/answers' },
+  { name: 'settings', path: '/admin/qa/settings' },
+];
+
+export const ADMIN_TAGS_NAV_MENUS = [
+  // { name: 'tags', path: '/admin/tags' },
+  {
+    name: 'settings',
+    path: '/admin/tags/settings',
+  },
+];
+
+export const ADMIN_USERS_NAV_MENUS = [
+  { name: 'users', path: '/admin/users' },
+  { name: 'settings', path: '/admin/users/settings' },
+];
+
+export const ADMIN_RULES_NAV_MENUS = [
+  { name: 'privileges', path: '/admin/rules/privileges' },
+  { name: 'policies', path: '/admin/rules/policies' },
+];
+
 export const TIMEZONES = [
   {
     label: 'Africa',
diff --git a/ui/src/common/interface.ts b/ui/src/common/interface.ts
index 3a77047e..aac89cce 100644
--- a/ui/src/common/interface.ts
+++ b/ui/src/common/interface.ts
@@ -364,7 +364,6 @@ export interface AdminSettingsGeneral {
   description: string;
   site_url: string;
   contact_email: string;
-  check_update: boolean;
   permalink?: number;
 }
 
@@ -382,8 +381,6 @@ export interface HelmetUpdate extends Omit<HelmetBase, 
'pageTitle'> {
 export interface AdminSettingsInterface {
   language: string;
   time_zone?: string;
-  default_avatar: string;
-  gravatar_base_url: string;
 }
 
 export interface AdminSettingsSmtp {
@@ -405,6 +402,14 @@ export interface AdminSettingsUsers {
   allow_update_location: boolean;
   allow_update_username: boolean;
   allow_update_website: boolean;
+  default_avatar: string;
+  gravatar_base_url: string;
+}
+
+export interface AdminSettingsSecurity {
+  external_content_display: string;
+  check_update: boolean;
+  login_required: boolean;
 }
 
 export interface SiteSettings {
@@ -416,10 +421,12 @@ export interface SiteSettings {
   theme: AdminSettingsTheme;
   site_seo: AdminSettingsSeo;
   site_users: AdminSettingsUsers;
-  site_write: AdminSettingsWrite;
+  site_advanced: AdminSettingsWrite;
+  site_questions: AdminQuestionSetting;
+  site_tags: AdminTagsSetting;
   version: string;
   revision: string;
-  site_legal: AdminSettingsLegal;
+  site_security: AdminSettingsSecurity;
 }
 
 export interface AdminSettingBranding {
@@ -430,7 +437,6 @@ export interface AdminSettingBranding {
 }
 
 export interface AdminSettingsLegal {
-  external_content_display: string;
   privacy_policy_original_text?: string;
   privacy_policy_parsed_text?: string;
   terms_of_service_original_text?: string;
@@ -438,12 +444,6 @@ export interface AdminSettingsLegal {
 }
 
 export interface AdminSettingsWrite {
-  restrict_answer?: boolean;
-  min_tags?: number;
-  min_content?: number;
-  recommend_tags?: Tag[];
-  required_tag?: boolean;
-  reserved_tags?: Tag[];
   max_image_size?: number;
   max_attachment_size?: number;
   max_image_megapixel?: number;
@@ -484,7 +484,6 @@ export interface AdminSettingsCustom {
 
 export interface AdminSettingsLogin {
   allow_new_registrations: boolean;
-  login_required: boolean;
   allow_email_registrations: boolean;
   allow_email_domains: string[];
   allow_password_login: boolean;
@@ -809,3 +808,15 @@ export interface BadgeDetailListRes {
   count: number;
   list: BadgeDetailListItem[];
 }
+
+export interface AdminQuestionSetting {
+  min_tags: number;
+  min_content: number;
+  restrict_answer: boolean;
+}
+
+export interface AdminTagsSetting {
+  recommend_tags: Tag[];
+  required_tag: boolean;
+  reserved_tags: Tag[];
+}
diff --git a/ui/src/components/AccordionNav/index.tsx 
b/ui/src/components/AccordionNav/index.tsx
index ee081978..583c0198 100644
--- a/ui/src/components/AccordionNav/index.tsx
+++ b/ui/src/components/AccordionNav/index.tsx
@@ -28,16 +28,32 @@ import { floppyNavigation } from '@/utils';
 import { Icon } from '@/components';
 import './index.css';
 
+export interface MenuItem {
+  name: string;
+  path?: string;
+  pathPrefix?: string;
+  icon?: string;
+  displayName?: string;
+  badgeContent?: string | number;
+  children?: MenuItem[];
+}
+
 function MenuNode({
   menu,
   callback,
   activeKey,
   expanding = false,
   path = '/',
+}: {
+  menu: MenuItem;
+  callback: (evt: any, menu: MenuItem, href: string, isLeaf: boolean) => void;
+  activeKey: string;
+  expanding?: boolean;
+  path?: string;
 }) {
   const { t } = useTranslation('translation', { keyPrefix: 'nav_menus' });
-  const isLeaf = !menu.children.length;
-  const href = isLeaf ? `${path}${menu.path}` : '#';
+  const isLeaf = !menu.children || menu.children.length === 0;
+  const href = isLeaf ? `${path}${menu.path || ''}` : '#';
 
   return (
     <Nav.Item key={menu.path} className="w-100">
@@ -51,7 +67,14 @@ function MenuNode({
           }}
           className={classNames(
             'text-nowrap d-flex flex-nowrap align-items-center w-100',
-            { expanding, active: activeKey === menu.path },
+            {
+              expanding,
+              active:
+                activeKey === menu.path ||
+                (menu.path && activeKey.startsWith(`${menu.path}/`)) ||
+                // if pathPrefix is set, activate when activeKey starts with 
the pathPrefix
+                (menu.pathPrefix && activeKey.startsWith(menu.pathPrefix)),
+            },
           )}>
           {menu?.icon && <Icon name={menu.icon} className="me-2" />}
 
@@ -75,7 +98,13 @@ function MenuNode({
           }}
           className={classNames(
             'text-nowrap d-flex flex-nowrap align-items-center w-100',
-            { expanding, active: activeKey === menu.path },
+            {
+              expanding,
+              active:
+                activeKey === menu.path ||
+                (menu.path && activeKey.startsWith(`${menu.path}/`)) ||
+                (menu.pathPrefix && activeKey.startsWith(menu.pathPrefix)),
+            },
           )}>
           {menu?.icon && <Icon name={menu.icon} className="me-2" />}
           <span className="me-auto text-truncate">
@@ -90,8 +119,8 @@ function MenuNode({
         </Nav.Link>
       )}
 
-      {menu.children.length ? (
-        <Accordion.Collapse eventKey={menu.path} className="ms-4">
+      {menu.children && menu.children.length > 0 ? (
+        <Accordion.Collapse eventKey={menu.path || menu.name} className="ms-4">
           <>
             {menu.children.map((leaf) => {
               return (
@@ -100,7 +129,7 @@ function MenuNode({
                   callback={callback}
                   activeKey={activeKey}
                   path={path}
-                  key={leaf.path}
+                  key={leaf.path || leaf.name}
                 />
               );
             })}
@@ -112,7 +141,7 @@ function MenuNode({
 }
 
 interface AccordionProps {
-  menus: any[];
+  menus: MenuItem[];
   path?: string;
 }
 const AccordionNav: FC<AccordionProps> = ({ menus = [], path = '/' }) => {
@@ -137,19 +166,27 @@ const AccordionNav: FC<AccordionProps> = ({ menus = [], 
path = '/' }) => {
   });
 
   const splat = pathMatch && pathMatch.params['*'];
-  let activeKey = menus[0].path;
+  let activeKey: string = menus[0]?.path || menus[0]?.name || '';
+
   if (splat) {
     activeKey = splat;
   }
+
   const getOpenKey = () => {
     let openKey = '';
     menus.forEach((li) => {
-      if (li.children.length) {
+      if (li.children && li.children.length > 0) {
         const matchedChild = li.children.find((el) => {
-          return el.path === activeKey;
+          // exact match or path prefix match
+          return (
+            el.path === activeKey ||
+            (el.path && activeKey.startsWith(`${el.path}/`)) ||
+            // if pathPrefix is set, activate when activeKey starts with the 
pathPrefix
+            (el.pathPrefix && activeKey.startsWith(el.pathPrefix))
+          );
         });
         if (matchedChild) {
-          openKey = li.path;
+          openKey = li.path || li.name || '';
         }
       }
     });
@@ -181,8 +218,8 @@ const AccordionNav: FC<AccordionProps> = ({ menus = [], 
path = '/' }) => {
               path={path}
               callback={menuClick}
               activeKey={activeKey}
-              expanding={openKey === li.path}
-              key={li.path}
+              expanding={openKey === (li.path || li.name)}
+              key={li.path || li.name}
             />
           );
         })}
diff --git a/ui/src/components/AdminSideNav/index.tsx 
b/ui/src/components/AdminSideNav/index.tsx
index a2d36fbd..b6c4d4bc 100644
--- a/ui/src/components/AdminSideNav/index.tsx
+++ b/ui/src/components/AdminSideNav/index.tsx
@@ -24,6 +24,7 @@ import { useTranslation } from 'react-i18next';
 import cloneDeep from 'lodash/cloneDeep';
 
 import { AccordionNav, Icon } from '@/components';
+import type { MenuItem } from '@/components/AccordionNav';
 import { ADMIN_NAV_MENUS } from '@/common/constants';
 import { useQueryPlugins } from '@/services';
 import { interfaceStore } from '@/stores';
@@ -37,16 +38,18 @@ const AdminSideNav = () => {
       have_config: true,
     });
 
-  const menus = cloneDeep(ADMIN_NAV_MENUS);
+  const menus = cloneDeep(ADMIN_NAV_MENUS) as MenuItem[];
   if (configurablePlugins && configurablePlugins.length > 0) {
     menus.forEach((item) => {
       if (item.name === 'plugins' && item.children) {
         item.children = [
           ...item.children,
-          ...configurablePlugins.map((plugin) => ({
-            name: plugin.slug_name,
-            displayName: plugin.name,
-          })),
+          ...configurablePlugins.map(
+            (plugin): MenuItem => ({
+              name: plugin.slug_name,
+              displayName: plugin.name,
+            }),
+          ),
         ];
       }
     });
diff --git a/ui/src/components/SchemaForm/components/Switch.tsx 
b/ui/src/components/SchemaForm/components/Switch.tsx
index 336142ae..81f928d3 100644
--- a/ui/src/components/SchemaForm/components/Switch.tsx
+++ b/ui/src/components/SchemaForm/components/Switch.tsx
@@ -51,6 +51,7 @@ const Index: FC<Props> = ({
       onChange(state);
     }
   };
+
   return (
     <Form.Check
       name={fieldName}
@@ -59,7 +60,7 @@ const Index: FC<Props> = ({
       checked={fieldObject?.value || ''}
       feedback={fieldObject?.errorMsg}
       feedbackType="invalid"
-      isInvalid={fieldObject.isInvalid}
+      isInvalid={fieldObject?.isInvalid}
       disabled={readOnly}
       onChange={handleChange}
     />
diff --git a/ui/src/components/SchemaForm/index.tsx 
b/ui/src/components/SchemaForm/index.tsx
index 171b5f67..01f50681 100644
--- a/ui/src/components/SchemaForm/index.tsx
+++ b/ui/src/components/SchemaForm/index.tsx
@@ -290,7 +290,7 @@ const SchemaForm: ForwardRefRenderFunction<FormRef, 
FormProps> = (
             controlId={key}
             className={classnames(
               groupClassName,
-              formData[key].hidden ? 'd-none' : null,
+              formData[key]?.hidden ? 'd-none' : null,
             )}>
             {/* Uniform processing `label` */}
             {title && !uiSimplify ? <Form.Label>{title}</Form.Label> : null}
@@ -437,12 +437,12 @@ const SchemaForm: ForwardRefRenderFunction<FormRef, 
FormProps> = (
               />
             ) : null}
             {/* Unified handling of `Feedback` and `Text` */}
-            <Form.Control.Feedback type="invalid">
-              {fieldState?.errorMsg}
-            </Form.Control.Feedback>
             {description && widget !== 'tag_selector' ? (
               <Form.Text dangerouslySetInnerHTML={{ __html: description }} />
             ) : null}
+            <Form.Control.Feedback type="invalid">
+              {fieldState?.errorMsg}
+            </Form.Control.Feedback>
           </Form.Group>
         );
       })}
diff --git a/ui/src/components/SchemaForm/types.ts 
b/ui/src/components/SchemaForm/types.ts
index 55cf8e99..25e5c56c 100644
--- a/ui/src/components/SchemaForm/types.ts
+++ b/ui/src/components/SchemaForm/types.ts
@@ -44,7 +44,7 @@ export interface JSONSchema {
   required?: string[];
   properties: {
     [key: string]: {
-      type?: 'string' | 'boolean' | 'number';
+      type?: 'string' | 'boolean' | 'number' | Type.Tag[];
       title: string;
       description?: string;
       enum?: Array<string | boolean | number>;
diff --git a/ui/src/components/TabNav/index.tsx 
b/ui/src/components/TabNav/index.tsx
new file mode 100644
index 00000000..482cd0cf
--- /dev/null
+++ b/ui/src/components/TabNav/index.tsx
@@ -0,0 +1,26 @@
+import { FC } from 'react';
+import { Nav } from 'react-bootstrap';
+import { NavLink, useLocation } from 'react-router-dom';
+import { useTranslation } from 'react-i18next';
+
+const TabNav: FC<{ menus: { name: string; path: string }[] }> = ({ menus }) => 
{
+  const { t } = useTranslation('translation', { keyPrefix: 'nav_menus' });
+  const { pathname } = useLocation();
+  return (
+    <Nav variant="underline" className="mb-4 border-bottom">
+      {menus.map((menu) => (
+        <Nav.Item key={menu.path}>
+          <NavLink
+            to={menu.path}
+            className={() =>
+              pathname === menu.path ? 'nav-link active' : 'nav-link'
+            }>
+            {t(menu.name)}
+          </NavLink>
+        </Nav.Item>
+      ))}
+    </Nav>
+  );
+};
+
+export default TabNav;
diff --git a/ui/src/components/index.ts b/ui/src/components/index.ts
index 68e863d2..416db241 100644
--- a/ui/src/components/index.ts
+++ b/ui/src/components/index.ts
@@ -64,6 +64,7 @@ import CardBadge from './CardBadge';
 import PinList from './PinList';
 import MobileSideNav from './MobileSideNav';
 import AdminSideNav from './AdminSideNav';
+import TabNav from './TabNav';
 
 export {
   Avatar,
@@ -115,5 +116,6 @@ export {
   PinList,
   MobileSideNav,
   AdminSideNav,
+  TabNav,
 };
 export type { EditorRef, JSONSchema, UISchema };
diff --git a/ui/src/pages/Admin/Answers/index.tsx 
b/ui/src/pages/Admin/Answers/index.tsx
index 07190024..9909739f 100644
--- a/ui/src/pages/Admin/Answers/index.tsx
+++ b/ui/src/pages/Admin/Answers/index.tsx
@@ -32,8 +32,9 @@ import {
   Empty,
   QueryGroup,
   Modal,
+  TabNav,
 } from '@/components';
-import { ADMIN_LIST_STATUS } from '@/common/constants';
+import { ADMIN_LIST_STATUS, ADMIN_QA_NAV_MENUS } from '@/common/constants';
 import * as Type from '@/common/interface';
 import { deletePermanently, useAnswerSearch } from '@/services';
 import { escapeRemove } from '@/utils';
@@ -96,7 +97,10 @@ const Answers: FC = () => {
   };
   return (
     <>
-      <h3 className="mb-4">{t('page_title')}</h3>
+      <h3 className="mb-4">
+        {t('page_title', { keyPrefix: 'admin.questions' })}
+      </h3>
+      <TabNav menus={ADMIN_QA_NAV_MENUS} />
       <div className="d-flex flex-wrap justify-content-between 
align-items-center">
         <Stack direction="horizontal" gap={3} className="mb-3">
           <QueryGroup
diff --git a/ui/src/pages/Admin/Branding/index.tsx 
b/ui/src/pages/Admin/Branding/index.tsx
index bec7fc0f..ce2417cb 100644
--- a/ui/src/pages/Admin/Branding/index.tsx
+++ b/ui/src/pages/Admin/Branding/index.tsx
@@ -169,13 +169,15 @@ const Index: FC = () => {
   return (
     <ImgViewer>
       <h3 className="mb-4">{t('page_title')}</h3>
-      <SchemaForm
-        schema={schema}
-        uiSchema={uiSchema}
-        formData={formData}
-        onSubmit={onSubmit}
-        onChange={handleOnChange}
-      />
+      <div className="max-w-748">
+        <SchemaForm
+          schema={schema}
+          uiSchema={uiSchema}
+          formData={formData}
+          onSubmit={onSubmit}
+          onChange={handleOnChange}
+        />
+      </div>
     </ImgViewer>
   );
 };
diff --git a/ui/src/pages/Admin/CssAndHtml/index.tsx 
b/ui/src/pages/Admin/CssAndHtml/index.tsx
index 422659af..557ffa3c 100644
--- a/ui/src/pages/Admin/CssAndHtml/index.tsx
+++ b/ui/src/pages/Admin/CssAndHtml/index.tsx
@@ -151,13 +151,15 @@ const Index: FC = () => {
   return (
     <>
       <h3 className="mb-4">{t('customize', { keyPrefix: 'nav_menus' })}</h3>
-      <SchemaForm
-        schema={schema}
-        formData={formData}
-        onSubmit={onSubmit}
-        uiSchema={uiSchema}
-        onChange={handleOnChange}
-      />
+      <div className="max-w-748">
+        <SchemaForm
+          schema={schema}
+          formData={formData}
+          onSubmit={onSubmit}
+          uiSchema={uiSchema}
+          onChange={handleOnChange}
+        />
+      </div>
     </>
   );
 };
diff --git a/ui/src/pages/Admin/Dashboard/components/HealthStatus/index.tsx 
b/ui/src/pages/Admin/Dashboard/components/HealthStatus/index.tsx
index 137fcd2e..9a79edd9 100644
--- a/ui/src/pages/Admin/Dashboard/components/HealthStatus/index.tsx
+++ b/ui/src/pages/Admin/Dashboard/components/HealthStatus/index.tsx
@@ -23,7 +23,7 @@ import { useTranslation } from 'react-i18next';
 import { Link } from 'react-router-dom';
 
 import type * as Type from '@/common/interface';
-import { siteInfoStore } from '@/stores';
+import { siteSecurityStore } from '@/stores';
 
 const { gt, gte } = require('semver');
 
@@ -34,7 +34,7 @@ interface IProps {
 const HealthStatus: FC<IProps> = ({ data }) => {
   const { t } = useTranslation('translation', { keyPrefix: 'admin.dashboard' 
});
   const { version, remote_version } = data.version_info || {};
-  const { siteInfo } = siteInfoStore();
+  const { check_update } = siteSecurityStore.getState();
   let isLatest = false;
   let hasNewerVersion = false;
   const downloadUrl = 
`https://answer.apache.org/download?from_version=${version}`;
@@ -68,7 +68,7 @@ const HealthStatus: FC<IProps> = ({ data }) => {
                 {t('update_to')} {remote_version}
               </a>
             )}
-            {!isLatest && !remote_version && siteInfo.check_update && (
+            {!isLatest && !remote_version && check_update && (
               <a
                 className="ms-1 badge rounded-pill text-bg-danger"
                 target="_blank"
diff --git a/ui/src/pages/Admin/Files/index.tsx 
b/ui/src/pages/Admin/Files/index.tsx
new file mode 100644
index 00000000..8c8650e8
--- /dev/null
+++ b/ui/src/pages/Admin/Files/index.tsx
@@ -0,0 +1,261 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { FC, useEffect, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { Form, Button } from 'react-bootstrap';
+
+import type * as Type from '@/common/interface';
+import { useToast } from '@/hooks';
+import { getAdminFilesSetting, updateAdminFilesSetting } from '@/services';
+import { handleFormError, scrollToElementTop } from '@/utils';
+import { writeSettingStore } from '@/stores';
+
+const initFormData = {
+  max_image_size: {
+    value: 0,
+    errorMsg: '',
+    isInvalid: false,
+  },
+  max_attachment_size: {
+    value: 0,
+    errorMsg: '',
+    isInvalid: false,
+  },
+  max_image_megapixel: {
+    value: 0,
+    errorMsg: '',
+    isInvalid: false,
+  },
+  authorized_image_extensions: {
+    value: '',
+    errorMsg: '',
+    isInvalid: false,
+  },
+  authorized_attachment_extensions: {
+    value: '',
+    errorMsg: '',
+    isInvalid: false,
+  },
+};
+
+const Index: FC = () => {
+  const { t } = useTranslation('translation', {
+    keyPrefix: 'admin.write',
+  });
+  const Toast = useToast();
+
+  const [formData, setFormData] = useState(initFormData);
+
+  const handleValueChange = (value) => {
+    setFormData({
+      ...formData,
+      ...value,
+    });
+  };
+
+  const onSubmit = (evt) => {
+    evt.preventDefault();
+    evt.stopPropagation();
+
+    const reqParams: Type.AdminSettingsWrite = {
+      max_image_size: Number(formData.max_image_size.value),
+      max_attachment_size: Number(formData.max_attachment_size.value),
+      max_image_megapixel: Number(formData.max_image_megapixel.value),
+      authorized_image_extensions:
+        formData.authorized_image_extensions.value?.length > 0
+          ? formData.authorized_image_extensions.value
+              .split(',')
+              ?.map((item) => item.trim().toLowerCase())
+          : [],
+      authorized_attachment_extensions:
+        formData.authorized_attachment_extensions.value?.length > 0
+          ? formData.authorized_attachment_extensions.value
+              .split(',')
+              ?.map((item) => item.trim().toLowerCase())
+          : [],
+    };
+    updateAdminFilesSetting(reqParams)
+      .then(() => {
+        Toast.onShow({
+          msg: t('update', { keyPrefix: 'toast' }),
+          variant: 'success',
+        });
+        writeSettingStore.getState().update({ ...reqParams });
+      })
+      .catch((err) => {
+        if (err.isError) {
+          const data = handleFormError(err, formData);
+          setFormData({ ...data });
+          const ele = document.getElementById(err.list[0].error_field);
+          scrollToElementTop(ele);
+        }
+      });
+  };
+
+  const initData = () => {
+    getAdminFilesSetting().then((res) => {
+      formData.max_image_size.value = res.max_image_size;
+      formData.max_attachment_size.value = res.max_attachment_size;
+      formData.max_image_megapixel.value = res.max_image_megapixel;
+      formData.authorized_image_extensions.value =
+        res.authorized_image_extensions?.join(', ').toLowerCase();
+      formData.authorized_attachment_extensions.value =
+        res.authorized_attachment_extensions?.join(', ').toLowerCase();
+      setFormData({ ...formData });
+    });
+  };
+
+  useEffect(() => {
+    initData();
+  }, []);
+
+  return (
+    <>
+      <h3 className="mb-4">{t('page_title')}</h3>
+      <div className="max-w-748">
+        <Form noValidate onSubmit={onSubmit}>
+          <Form.Group className="mb-3" controlId="max_image_size">
+            <Form.Label>{t('image_size.label')}</Form.Label>
+            <Form.Control
+              type="number"
+              inputMode="numeric"
+              min={0}
+              value={formData.max_image_size.value}
+              isInvalid={formData.max_image_size.isInvalid}
+              onChange={(evt) => {
+                handleValueChange({
+                  max_image_size: {
+                    value: evt.target.value,
+                    errorMsg: '',
+                    isInvalid: false,
+                  },
+                });
+              }}
+            />
+            <Form.Text>{t('image_size.text')}</Form.Text>
+            <Form.Control.Feedback type="invalid">
+              {formData.max_image_size.errorMsg}
+            </Form.Control.Feedback>
+          </Form.Group>
+
+          <Form.Group className="mb-3" controlId="max_attachment_size">
+            <Form.Label>{t('attachment_size.label')}</Form.Label>
+            <Form.Control
+              type="number"
+              inputMode="numeric"
+              min={0}
+              value={formData.max_attachment_size.value}
+              isInvalid={formData.max_attachment_size.isInvalid}
+              onChange={(evt) => {
+                handleValueChange({
+                  max_attachment_size: {
+                    value: evt.target.value,
+                    errorMsg: '',
+                    isInvalid: false,
+                  },
+                });
+              }}
+            />
+            <Form.Text>{t('attachment_size.text')}</Form.Text>
+            <Form.Control.Feedback type="invalid">
+              {formData.max_attachment_size.errorMsg}
+            </Form.Control.Feedback>
+          </Form.Group>
+
+          <Form.Group className="mb-3" controlId="max_image_megapixel">
+            <Form.Label>{t('image_megapixels.label')}</Form.Label>
+            <Form.Control
+              type="number"
+              inputMode="numeric"
+              min={0}
+              isInvalid={formData.max_image_megapixel.isInvalid}
+              value={formData.max_image_megapixel.value}
+              onChange={(evt) => {
+                handleValueChange({
+                  max_image_megapixel: {
+                    value: evt.target.value,
+                    errorMsg: '',
+                    isInvalid: false,
+                  },
+                });
+              }}
+            />
+            <Form.Text>{t('image_megapixels.text')}</Form.Text>
+            <Form.Control.Feedback type="invalid">
+              {formData.max_image_megapixel.errorMsg}
+            </Form.Control.Feedback>
+          </Form.Group>
+
+          <Form.Group className="mb-3" controlId="authorized_image_extensions">
+            <Form.Label>{t('image_extensions.label')}</Form.Label>
+            <Form.Control
+              type="text"
+              value={formData.authorized_image_extensions.value}
+              isInvalid={formData.authorized_image_extensions.isInvalid}
+              onChange={(evt) => {
+                handleValueChange({
+                  authorized_image_extensions: {
+                    value: evt.target.value.toLowerCase(),
+                    errorMsg: '',
+                    isInvalid: false,
+                  },
+                });
+              }}
+            />
+            <Form.Text>{t('image_extensions.text')}</Form.Text>
+            <Form.Control.Feedback type="invalid">
+              {formData.authorized_image_extensions.errorMsg}
+            </Form.Control.Feedback>
+          </Form.Group>
+
+          <Form.Group
+            className="mb-3"
+            controlId="authorized_attachment_extensions">
+            <Form.Label>{t('attachment_extensions.label')}</Form.Label>
+            <Form.Control
+              type="text"
+              value={formData.authorized_attachment_extensions.value}
+              isInvalid={formData.authorized_attachment_extensions.isInvalid}
+              onChange={(evt) => {
+                handleValueChange({
+                  authorized_attachment_extensions: {
+                    value: evt.target.value.toLowerCase(),
+                    errorMsg: '',
+                    isInvalid: false,
+                  },
+                });
+              }}
+            />
+            <Form.Text>{t('attachment_extensions.text')}</Form.Text>
+            <Form.Control.Feedback type="invalid">
+              {formData.authorized_attachment_extensions.errorMsg}
+            </Form.Control.Feedback>
+          </Form.Group>
+
+          <Form.Group className="mb-3">
+            <Button type="submit">{t('save', { keyPrefix: 'btns' })}</Button>
+          </Form.Group>
+        </Form>
+      </div>
+    </>
+  );
+};
+
+export default Index;
diff --git a/ui/src/pages/Admin/General/index.tsx 
b/ui/src/pages/Admin/General/index.tsx
index ce2cdeb5..57602f45 100644
--- a/ui/src/pages/Admin/General/index.tsx
+++ b/ui/src/pages/Admin/General/index.tsx
@@ -70,11 +70,6 @@ const General: FC = () => {
         title: t('contact_email.label'),
         description: t('contact_email.text'),
       },
-      check_update: {
-        type: 'boolean',
-        title: t('check_update.label'),
-        default: true,
-      },
     },
   };
   const uiSchema: UISchema = {
@@ -114,12 +109,6 @@ const General: FC = () => {
         },
       },
     },
-    check_update: {
-      'ui:widget': 'switch',
-      'ui:options': {
-        label: t('check_update.text'),
-      },
-    },
   };
   const [formData, setFormData] = useState<Type.FormDataType>(
     initFormData(schema),
@@ -134,7 +123,6 @@ const General: FC = () => {
       short_description: formData.short_description.value,
       site_url: formData.site_url.value,
       contact_email: formData.contact_email.value,
-      check_update: formData.check_update.value,
     };
 
     updateGeneralSetting(reqParams)
@@ -149,7 +137,6 @@ const General: FC = () => {
           formData.short_description.value = res.short_description;
           formData.site_url.value = res.site_url;
           formData.contact_email.value = res.contact_email;
-          formData.check_update.value = res.check_update;
         }
 
         setFormData({ ...formData });
@@ -183,13 +170,15 @@ const General: FC = () => {
   return (
     <>
       <h3 className="mb-4">{t('page_title')}</h3>
-      <SchemaForm
-        schema={schema}
-        formData={formData}
-        onSubmit={onSubmit}
-        uiSchema={uiSchema}
-        onChange={handleOnChange}
-      />
+      <div className="max-w-748">
+        <SchemaForm
+          schema={schema}
+          formData={formData}
+          onSubmit={onSubmit}
+          uiSchema={uiSchema}
+          onChange={handleOnChange}
+        />
+      </div>
     </>
   );
 };
diff --git a/ui/src/pages/Admin/Interface/index.tsx 
b/ui/src/pages/Admin/Interface/index.tsx
index 865029e9..49e5175d 100644
--- a/ui/src/pages/Admin/Interface/index.tsx
+++ b/ui/src/pages/Admin/Interface/index.tsx
@@ -28,7 +28,7 @@ import {
 } from '@/common/interface';
 import { interfaceStore, loggedUserInfoStore } from '@/stores';
 import { JSONSchema, SchemaForm, UISchema } from '@/components';
-import { DEFAULT_TIMEZONE, SYSTEM_AVATAR_OPTIONS } from '@/common/constants';
+import { DEFAULT_TIMEZONE } from '@/common/constants';
 import {
   updateInterfaceSetting,
   useInterfaceSetting,
@@ -68,20 +68,6 @@ const Interface: FC = () => {
         description: t('time_zone.text'),
         default: setting?.time_zone || DEFAULT_TIMEZONE,
       },
-      default_avatar: {
-        type: 'string',
-        title: t('avatar.label'),
-        description: t('avatar.text'),
-        enum: SYSTEM_AVATAR_OPTIONS?.map((v) => v.value),
-        enumNames: SYSTEM_AVATAR_OPTIONS?.map((v) => v.label),
-        default: setting?.default_avatar || 'system',
-      },
-      gravatar_base_url: {
-        type: 'string',
-        title: t('gravatar_base_url.label'),
-        description: t('gravatar_base_url.text'),
-        default: setting?.gravatar_base_url || '',
-      },
     },
   };
 
@@ -96,16 +82,6 @@ const Interface: FC = () => {
       isInvalid: false,
       errorMsg: '',
     },
-    default_avatar: {
-      value: setting?.default_avatar || 'system',
-      isInvalid: false,
-      errorMsg: '',
-    },
-    gravatar_base_url: {
-      value: setting?.gravatar_base_url || '',
-      isInvalid: false,
-      errorMsg: '',
-    },
   });
 
   const uiSchema: UISchema = {
@@ -115,15 +91,6 @@ const Interface: FC = () => {
     time_zone: {
       'ui:widget': 'timezone',
     },
-    default_avatar: {
-      'ui:widget': 'select',
-    },
-    gravatar_base_url: {
-      'ui:widget': 'input',
-      'ui:options': {
-        placeholder: 'https://www.gravatar.com/avatar/',
-      },
-    },
   };
   const getLangs = async () => {
     const res: LangsType[] = await loadLanguageOptions(true);
@@ -156,8 +123,6 @@ const Interface: FC = () => {
     const reqParams: AdminSettingsInterface = {
       language: formData.language.value,
       time_zone: formData.time_zone.value,
-      default_avatar: formData.default_avatar.value,
-      gravatar_base_url: formData.gravatar_base_url.value,
     };
 
     updateInterfaceSetting(reqParams)
@@ -185,20 +150,18 @@ const Interface: FC = () => {
 
   useEffect(() => {
     if (setting) {
-      const formMeta = {};
-      Object.keys(setting).forEach((k) => {
-        let v = setting[k];
-        if (k === 'default_avatar' && !v) {
-          v = 'system';
-        }
-        if (k === 'gravatar_base_url' && !v) {
-          v = '';
-        }
-        formMeta[k] = { ...formData[k], value: v };
-      });
+      const formMeta = { ...formData };
+      if (setting.language) {
+        formMeta.language.value = setting.language;
+      } else {
+        formMeta.language.value = storeInterface.language || langs?.[0]?.value;
+      }
+      if (setting.time_zone) {
+        formMeta.time_zone.value = setting.time_zone;
+      }
       setFormData({ ...formData, ...formMeta });
     }
-  }, [setting]);
+  }, [setting, langs]);
   useEffect(() => {
     getLangs();
   }, []);
@@ -209,13 +172,15 @@ const Interface: FC = () => {
   return (
     <>
       <h3 className="mb-4">{t('page_title')}</h3>
-      <SchemaForm
-        schema={schema}
-        uiSchema={uiSchema}
-        formData={formData}
-        onSubmit={onSubmit}
-        onChange={handleOnChange}
-      />
+      <div className="max-w-748">
+        <SchemaForm
+          schema={schema}
+          uiSchema={uiSchema}
+          formData={formData}
+          onSubmit={onSubmit}
+          onChange={handleOnChange}
+        />
+      </div>
     </>
   );
 };
diff --git a/ui/src/pages/Admin/Login/index.tsx 
b/ui/src/pages/Admin/Login/index.tsx
index c43cfcff..a4a61152 100644
--- a/ui/src/pages/Admin/Login/index.tsx
+++ b/ui/src/pages/Admin/Login/index.tsx
@@ -58,12 +58,6 @@ const Index: FC = () => {
         title: t('allowed_email_domains.title'),
         description: t('allowed_email_domains.text'),
       },
-      login_required: {
-        type: 'boolean',
-        title: t('private.title'),
-        description: t('private.text'),
-        default: false,
-      },
     },
   };
   const uiSchema: UISchema = {
@@ -88,12 +82,6 @@ const Index: FC = () => {
     allow_email_domains: {
       'ui:widget': 'textarea',
     },
-    login_required: {
-      'ui:widget': 'switch',
-      'ui:options': {
-        label: t('private.label'),
-      },
-    },
   };
   const [formData, setFormData] = useState(initFormData(schema));
   const { update: updateLoginSetting } = loginSettingStore((_) => _);
@@ -116,7 +104,6 @@ const Index: FC = () => {
       allow_new_registrations: formData.allow_new_registrations.value,
       allow_email_registrations: formData.allow_email_registrations.value,
       allow_email_domains: allowedEmailDomains,
-      login_required: formData.login_required.value,
       allow_password_login: formData.allow_password_login.value,
     };
 
@@ -151,7 +138,6 @@ const Index: FC = () => {
           formMeta.allow_email_domains.value =
             setting.allow_email_domains.join('\n');
         }
-        formMeta.login_required.value = setting.login_required;
         formMeta.allow_password_login.value = setting.allow_password_login;
         setFormData({ ...formMeta });
       }
@@ -165,13 +151,15 @@ const Index: FC = () => {
   return (
     <>
       <h3 className="mb-4">{t('page_title')}</h3>
-      <SchemaForm
-        schema={schema}
-        formData={formData}
-        onSubmit={onSubmit}
-        uiSchema={uiSchema}
-        onChange={handleOnChange}
-      />
+      <div className="max-w-748">
+        <SchemaForm
+          schema={schema}
+          formData={formData}
+          onSubmit={onSubmit}
+          uiSchema={uiSchema}
+          onChange={handleOnChange}
+        />
+      </div>
     </>
   );
 };
diff --git a/ui/src/pages/Admin/Plugins/Config/index.tsx 
b/ui/src/pages/Admin/Plugins/Config/index.tsx
index 7c47d016..44d83ad6 100644
--- a/ui/src/pages/Admin/Plugins/Config/index.tsx
+++ b/ui/src/pages/Admin/Plugins/Config/index.tsx
@@ -114,14 +114,16 @@ const Config = () => {
   return (
     <>
       <h3 className="mb-4">{data?.name}</h3>
-      <SchemaForm
-        schema={schema}
-        uiSchema={uiSchema}
-        refreshConfig={refreshConfig}
-        formData={formData}
-        onSubmit={onSubmit}
-        onChange={handleOnChange}
-      />
+      <div className="max-w-748">
+        <SchemaForm
+          schema={schema}
+          uiSchema={uiSchema}
+          refreshConfig={refreshConfig}
+          formData={formData}
+          onSubmit={onSubmit}
+          onChange={handleOnChange}
+        />
+      </div>
     </>
   );
 };
diff --git a/ui/src/pages/Admin/Legal/index.tsx 
b/ui/src/pages/Admin/Policies/index.tsx
similarity index 70%
rename from ui/src/pages/Admin/Legal/index.tsx
rename to ui/src/pages/Admin/Policies/index.tsx
index 4a4e4ea1..7170c39f 100644
--- a/ui/src/pages/Admin/Legal/index.tsx
+++ b/ui/src/pages/Admin/Policies/index.tsx
@@ -23,11 +23,17 @@ import { useTranslation } from 'react-i18next';
 import { marked } from 'marked';
 
 import type * as Type from '@/common/interface';
-import { SchemaForm, JSONSchema, initFormData, UISchema } from '@/components';
+import {
+  SchemaForm,
+  JSONSchema,
+  initFormData,
+  UISchema,
+  TabNav,
+} from '@/components';
 import { useToast } from '@/hooks';
-import { getLegalSetting, putLegalSetting } from '@/services';
+import { getPoliciesSetting, putPoliciesSetting } from '@/services';
 import { handleFormError, scrollToElementTop } from '@/utils';
-import { siteLealStore } from '@/stores';
+import { ADMIN_RULES_NAV_MENUS } from '@/common/constants';
 
 const Legal: FC = () => {
   const { t } = useTranslation('translation', {
@@ -35,29 +41,10 @@ const Legal: FC = () => {
   });
   const Toast = useToast();
 
-  const externalContent = [
-    {
-      value: 'always_display',
-      label: t('external_content_display.always_display'),
-    },
-    {
-      value: 'ask_before_display',
-      label: t('external_content_display.ask_before_display'),
-    },
-  ];
-
   const schema: JSONSchema = {
     title: t('page_title'),
     required: ['terms_of_service', 'privacy_policy'],
     properties: {
-      external_content_display: {
-        type: 'string',
-        title: t('external_content_display.label'),
-        description: t('external_content_display.text'),
-        enum: externalContent?.map((lang) => lang.value),
-        enumNames: externalContent?.map((lang) => lang.label),
-        default: 0,
-      },
       terms_of_service: {
         type: 'string',
         title: t('terms_of_service.label'),
@@ -71,9 +58,6 @@ const Legal: FC = () => {
     },
   };
   const uiSchema: UISchema = {
-    external_content_display: {
-      'ui:widget': 'select',
-    },
     terms_of_service: {
       'ui:widget': 'textarea',
       'ui:options': {
@@ -94,7 +78,6 @@ const Legal: FC = () => {
     evt.stopPropagation();
 
     const reqParams: Type.AdminSettingsLegal = {
-      external_content_display: formData.external_content_display.value,
       terms_of_service_original_text: formData.terms_of_service.value,
       terms_of_service_parsed_text: marked.parse(
         formData.terms_of_service.value,
@@ -103,15 +86,12 @@ const Legal: FC = () => {
       privacy_policy_parsed_text: marked.parse(formData.privacy_policy.value),
     };
 
-    putLegalSetting(reqParams)
+    putPoliciesSetting(reqParams)
       .then(() => {
         Toast.onShow({
           msg: t('update', { keyPrefix: 'toast' }),
           variant: 'success',
         });
-        siteLealStore.getState().update({
-          external_content_display: reqParams.external_content_display,
-        });
       })
       .catch((err) => {
         if (err.isError) {
@@ -124,11 +104,9 @@ const Legal: FC = () => {
   };
 
   useEffect(() => {
-    getLegalSetting().then((setting) => {
+    getPoliciesSetting().then((setting) => {
       if (setting) {
         const formMeta = { ...formData };
-        formMeta.external_content_display.value =
-          setting.external_content_display;
         formMeta.terms_of_service.value =
           setting.terms_of_service_original_text;
         formMeta.privacy_policy.value = setting.privacy_policy_original_text;
@@ -143,14 +121,17 @@ const Legal: FC = () => {
 
   return (
     <>
-      <h3 className="mb-4">{t('page_title')}</h3>
-      <SchemaForm
-        schema={schema}
-        formData={formData}
-        onSubmit={onSubmit}
-        uiSchema={uiSchema}
-        onChange={handleOnChange}
-      />
+      <h3 className="mb-4">{t('rules', { keyPrefix: 'nav_menus' })}</h3>
+      <TabNav menus={ADMIN_RULES_NAV_MENUS} />
+      <div className="max-w-748">
+        <SchemaForm
+          schema={schema}
+          formData={formData}
+          onSubmit={onSubmit}
+          uiSchema={uiSchema}
+          onChange={handleOnChange}
+        />
+      </div>
     </>
   );
 };
diff --git a/ui/src/pages/Admin/Privileges/index.tsx 
b/ui/src/pages/Admin/Privileges/index.tsx
index f0930c7a..9ab775dc 100644
--- a/ui/src/pages/Admin/Privileges/index.tsx
+++ b/ui/src/pages/Admin/Privileges/index.tsx
@@ -22,7 +22,13 @@ import { useTranslation } from 'react-i18next';
 
 import { useToast } from '@/hooks';
 import { FormDataType } from '@/common/interface';
-import { JSONSchema, SchemaForm, UISchema, initFormData } from '@/components';
+import {
+  JSONSchema,
+  SchemaForm,
+  UISchema,
+  initFormData,
+  TabNav,
+} from '@/components';
 import {
   getPrivilegeSetting,
   putPrivilegeSetting,
@@ -30,7 +36,10 @@ import {
   AdminSettingsPrivilegeReq,
 } from '@/services';
 import { handleFormError, scrollToElementTop } from '@/utils';
-import { ADMIN_PRIVILEGE_CUSTOM_LEVEL } from '@/common/constants';
+import {
+  ADMIN_PRIVILEGE_CUSTOM_LEVEL,
+  ADMIN_RULES_NAV_MENUS,
+} from '@/common/constants';
 
 const Index: FC = () => {
   const { t } = useTranslation('translation', {
@@ -187,14 +196,17 @@ const Index: FC = () => {
 
   return (
     <>
-      <h3 className="mb-4">{t('title')}</h3>
-      <SchemaForm
-        schema={schema}
-        uiSchema={uiSchema}
-        formData={formData}
-        onSubmit={onSubmit}
-        onChange={handleOnChange}
-      />
+      <h3 className="mb-4">{t('rules', { keyPrefix: 'nav_menus' })}</h3>
+      <TabNav menus={ADMIN_RULES_NAV_MENUS} />
+      <div className="max-w-748">
+        <SchemaForm
+          schema={schema}
+          uiSchema={uiSchema}
+          formData={formData}
+          onSubmit={onSubmit}
+          onChange={handleOnChange}
+        />
+      </div>
     </>
   );
 };
diff --git a/ui/src/pages/Admin/QaSettings/index.tsx 
b/ui/src/pages/Admin/QaSettings/index.tsx
new file mode 100644
index 00000000..0c2636ea
--- /dev/null
+++ b/ui/src/pages/Admin/QaSettings/index.tsx
@@ -0,0 +1,132 @@
+import { useState, useEffect } from 'react';
+import { useTranslation } from 'react-i18next';
+
+import {
+  SchemaForm,
+  JSONSchema,
+  UISchema,
+  initFormData,
+  TabNav,
+} from '@/components';
+import { ADMIN_QA_NAV_MENUS } from '@/common/constants';
+import * as Type from '@/common/interface';
+import { writeSettingStore } from '@/stores';
+import {
+  getQuestionSetting,
+  updateQuestionSetting,
+} from '@/services/admin/question';
+import { handleFormError, scrollToElementTop } from '@/utils';
+import { useToast } from '@/hooks';
+
+const QaSettings = () => {
+  const { t } = useTranslation('translation', {
+    keyPrefix: 'admin.write',
+  });
+  const Toast = useToast();
+  const schema: JSONSchema = {
+    title: t('page_title'),
+    properties: {
+      min_tags: {
+        type: 'number',
+        title: t('min_tags.label'),
+        description: t('min_tags.text'),
+      },
+      min_content: {
+        type: 'number',
+        title: t('min_content.label'),
+        description: t('min_content.text'),
+      },
+      restrict_answer: {
+        type: 'boolean',
+        title: t('restrict_answer.label'),
+        description: t('restrict_answer.text'),
+      },
+    },
+  };
+  const uiSchema: UISchema = {
+    min_tags: {
+      'ui:widget': 'input',
+      'ui:options': {
+        inputType: 'number',
+      },
+    },
+    min_content: {
+      'ui:widget': 'input',
+      'ui:options': {
+        inputType: 'number',
+      },
+    },
+    restrict_answer: {
+      'ui:widget': 'switch',
+      'ui:options': {
+        label: t('restrict_answer.label'),
+      },
+    },
+  };
+  const [formData, setFormData] = useState<Type.FormDataType>(
+    initFormData(schema),
+  );
+
+  const handleValueChange = (data: Type.FormDataType) => {
+    setFormData(data);
+  };
+
+  const onSubmit = (evt) => {
+    evt.preventDefault();
+    evt.stopPropagation();
+    // TODO: submit data
+    const reqParams: Type.AdminQuestionSetting = {
+      min_tags: formData.min_tags.value,
+      min_content: formData.min_content.value,
+      restrict_answer: formData.restrict_answer.value,
+    };
+    updateQuestionSetting(reqParams)
+      .then(() => {
+        Toast.onShow({
+          msg: t('update', { keyPrefix: 'toast' }),
+          variant: 'success',
+        });
+        writeSettingStore.getState().update({ ...reqParams });
+      })
+      .catch((err) => {
+        if (err.isError) {
+          const data = handleFormError(err, formData);
+          setFormData({ ...data });
+          const ele = document.getElementById(err.list[0].error_field);
+          scrollToElementTop(ele);
+        }
+      });
+  };
+
+  useEffect(() => {
+    getQuestionSetting().then((res) => {
+      if (res) {
+        const formMeta = { ...formData };
+        formMeta.min_tags.value = res.min_tags;
+        formMeta.min_content.value = res.min_content;
+        formMeta.restrict_answer.value = res.restrict_answer;
+        setFormData(formMeta);
+      }
+    });
+  }, []);
+
+  return (
+    <>
+      <h3 className="mb-4">
+        {t('page_title', { keyPrefix: 'admin.questions' })}
+      </h3>
+      <TabNav menus={ADMIN_QA_NAV_MENUS} />
+      <div className="max-w-748">
+        <SchemaForm
+          schema={schema}
+          uiSchema={uiSchema}
+          formData={formData}
+          onChange={handleValueChange}
+          onSubmit={onSubmit}
+        />
+      </div>
+    </>
+  );
+};
+
+export default QaSettings;
diff --git a/ui/src/pages/Admin/Questions/index.tsx 
b/ui/src/pages/Admin/Questions/index.tsx
index 494effe6..ffd9f609 100644
--- a/ui/src/pages/Admin/Questions/index.tsx
+++ b/ui/src/pages/Admin/Questions/index.tsx
@@ -32,8 +32,9 @@ import {
   Empty,
   QueryGroup,
   Modal,
+  TabNav,
 } from '@/components';
-import { ADMIN_LIST_STATUS } from '@/common/constants';
+import { ADMIN_LIST_STATUS, ADMIN_QA_NAV_MENUS } from '@/common/constants';
 import * as Type from '@/common/interface';
 import { deletePermanently, useQuestionSearch } from '@/services';
 import { pathFactory } from '@/router/pathFactory';
@@ -95,6 +96,7 @@ const Questions: FC = () => {
   return (
     <>
       <h3 className="mb-4">{t('page_title')}</h3>
+      <TabNav menus={ADMIN_QA_NAV_MENUS} />
       <div className="d-flex flex-wrap justify-content-between 
align-items-center">
         <Stack direction="horizontal" gap={3} className="mb-3">
           <QueryGroup
diff --git a/ui/src/pages/Admin/Security/index.tsx 
b/ui/src/pages/Admin/Security/index.tsx
new file mode 100644
index 00000000..07eaeb5b
--- /dev/null
+++ b/ui/src/pages/Admin/Security/index.tsx
@@ -0,0 +1,145 @@
+import { useState, useEffect } from 'react';
+import { useTranslation } from 'react-i18next';
+
+import type * as Type from '@/common/interface';
+import { SchemaForm, JSONSchema, initFormData, UISchema } from '@/components';
+import { siteSecurityStore } from '@/stores';
+import {
+  getSecuritySetting,
+  putSecuritySetting,
+} from '@/services/admin/settings';
+import { handleFormError, scrollToElementTop } from '@/utils';
+import { useToast } from '@/hooks';
+
+const Security = () => {
+  const { t } = useTranslation('translation', {
+    keyPrefix: 'admin.security',
+  });
+  const Toast = useToast();
+  const externalContent = [
+    {
+      value: 'always_display',
+      label: t('external_content_display.always_display', {
+        keyPrefix: 'admin.legal',
+      }),
+    },
+    {
+      value: 'ask_before_display',
+      label: t('external_content_display.ask_before_display', {
+        keyPrefix: 'admin.legal',
+      }),
+    },
+  ];
+
+  const schema: JSONSchema = {
+    title: t('page_title'),
+    properties: {
+      login_required: {
+        type: 'boolean',
+        title: t('private.title', { keyPrefix: 'admin.login' }),
+        description: t('private.text', { keyPrefix: 'admin.login' }),
+        default: false,
+      },
+      external_content_display: {
+        type: 'string',
+        title: t('external_content_display.label', {
+          keyPrefix: 'admin.legal',
+        }),
+        description: t('external_content_display.text', {
+          keyPrefix: 'admin.legal',
+        }),
+        enum: externalContent?.map((lang) => lang.value),
+        enumNames: externalContent?.map((lang) => lang.label),
+        default: 0,
+      },
+      check_update: {
+        type: 'boolean',
+        title: t('check_update.label', { keyPrefix: 'admin.general' }),
+        default: true,
+      },
+    },
+  };
+  const uiSchema: UISchema = {
+    login_required: {
+      'ui:widget': 'switch',
+      'ui:options': {
+        label: t('private.label', { keyPrefix: 'admin.login' }),
+      },
+    },
+    external_content_display: {
+      'ui:widget': 'select',
+      'ui:options': {
+        label: t('external_content_display.label', {
+          keyPrefix: 'admin.legal',
+        }),
+      },
+    },
+    check_update: {
+      'ui:widget': 'switch',
+      'ui:options': {
+        label: t('check_update.label', { keyPrefix: 'admin.general' }),
+      },
+    },
+  };
+  const [formData, setFormData] = useState(initFormData(schema));
+
+  const handleValueChange = (data: Type.FormDataType) => {
+    setFormData(data);
+  };
+
+  const onSubmit = (evt) => {
+    evt.preventDefault();
+    evt.stopPropagation();
+    const reqParams = {
+      login_required: formData.login_required.value,
+      external_content_display: formData.external_content_display.value,
+      check_update: formData.check_update.value,
+    };
+    putSecuritySetting(reqParams)
+      .then(() => {
+        Toast.onShow({
+          msg: t('update', { keyPrefix: 'toast' }),
+          variant: 'success',
+        });
+        siteSecurityStore.getState().update(reqParams);
+      })
+      .catch((err) => {
+        if (err.isError) {
+          const data = handleFormError(err, formData);
+          setFormData({ ...data });
+          const ele = document.getElementById(err.list[0].error_field);
+          scrollToElementTop(ele);
+        }
+      });
+  };
+
+  useEffect(() => {
+    getSecuritySetting().then((setting) => {
+      if (setting) {
+        const formMeta = { ...formData };
+        formMeta.login_required.value = setting.login_required;
+        formMeta.external_content_display.value =
+          setting.external_content_display;
+        formMeta.check_update.value = setting.check_update;
+        setFormData(formMeta);
+      }
+    });
+  }, []);
+
+  return (
+    <>
+      <h3 className="mb-4">{t('security', { keyPrefix: 'nav_menus' })}</h3>
+      <div className="max-w-748">
+        <SchemaForm
+          schema={schema}
+          uiSchema={uiSchema}
+          formData={formData}
+          onChange={handleValueChange}
+          onSubmit={onSubmit}
+        />
+      </div>
+    </>
+  );
+};
+
+export default Security;
diff --git a/ui/src/pages/Admin/Seo/index.tsx b/ui/src/pages/Admin/Seo/index.tsx
index e539595b..0675479d 100644
--- a/ui/src/pages/Admin/Seo/index.tsx
+++ b/ui/src/pages/Admin/Seo/index.tsx
@@ -117,13 +117,15 @@ const Index: FC = () => {
   return (
     <>
       <h3 className="mb-4">{t('page_title')}</h3>
-      <SchemaForm
-        schema={schema}
-        formData={formData}
-        onSubmit={onSubmit}
-        uiSchema={uiSchema}
-        onChange={handleOnChange}
-      />
+      <div className="max-w-748">
+        <SchemaForm
+          schema={schema}
+          formData={formData}
+          onSubmit={onSubmit}
+          uiSchema={uiSchema}
+          onChange={handleOnChange}
+        />
+      </div>
     </>
   );
 };
diff --git a/ui/src/pages/Admin/Smtp/index.tsx 
b/ui/src/pages/Admin/Smtp/index.tsx
index 85a1b976..59538751 100644
--- a/ui/src/pages/Admin/Smtp/index.tsx
+++ b/ui/src/pages/Admin/Smtp/index.tsx
@@ -224,13 +224,15 @@ const Smtp: FC = () => {
   return (
     <>
       <h3 className="mb-4">{t('page_title')}</h3>
-      <SchemaForm
-        schema={schema}
-        uiSchema={uiSchema}
-        formData={formData}
-        onChange={handleOnChange}
-        onSubmit={onSubmit}
-      />
+      <div className="max-w-748">
+        <SchemaForm
+          schema={schema}
+          uiSchema={uiSchema}
+          formData={formData}
+          onChange={handleOnChange}
+          onSubmit={onSubmit}
+        />
+      </div>
     </>
   );
 };
diff --git a/ui/src/pages/Admin/TagsSettings/index.tsx 
b/ui/src/pages/Admin/TagsSettings/index.tsx
new file mode 100644
index 00000000..f2ab2f5b
--- /dev/null
+++ b/ui/src/pages/Admin/TagsSettings/index.tsx
@@ -0,0 +1,170 @@
+import { useState, useEffect } from 'react';
+import { useTranslation } from 'react-i18next';
+
+import {
+  SchemaForm,
+  JSONSchema,
+  UISchema,
+  initFormData,
+  TabNav,
+} from '@/components';
+import { ADMIN_TAGS_NAV_MENUS } from '@/common/constants';
+import * as Type from '@/common/interface';
+import { handleFormError, scrollToElementTop } from '@/utils';
+import { writeSettingStore } from '@/stores';
+import { getAdminTagsSetting, updateAdminTagsSetting } from '@/services/admin';
+import { useToast } from '@/hooks';
+
+const QaSettings = () => {
+  const { t } = useTranslation('translation', {
+    keyPrefix: 'admin.write',
+  });
+  const Toast = useToast();
+  const schema: JSONSchema = {
+    title: t('page_title'),
+    properties: {
+      reserved_tags: {
+        type: 'string',
+        title: t('reserved_tags.label'),
+        description: t('reserved_tags.text'),
+      },
+      recommend_tags: {
+        type: 'string',
+        title: t('recommend_tags.label'),
+        description: t('recommend_tags.text'),
+      },
+      required_tag: {
+        type: 'boolean',
+        title: t('required_tag.title'),
+        description: t('required_tag.text'),
+      },
+    },
+  };
+  const uiSchema: UISchema = {
+    reserved_tags: {
+      'ui:widget': 'tag_selector',
+      'ui:options': {
+        label: t('reserved_tags.label'),
+      },
+    },
+    recommend_tags: {
+      'ui:widget': 'tag_selector',
+      'ui:options': {
+        label: t('recommend_tags.label'),
+      },
+    },
+    required_tag: {
+      'ui:widget': 'switch',
+      'ui:options': {
+        label: t('required_tag.label'),
+      },
+    },
+  };
+  const [formData, setFormData] = useState<Type.FormDataType>(
+    initFormData(schema),
+  );
+
+  const handleValueChange = (data: Type.FormDataType) => {
+    setFormData(data);
+  };
+
+  const checkValidated = (): boolean => {
+    let bol = true;
+    const { recommend_tags, reserved_tags } = formData;
+    // 找出 recommend_tags 和 reserved_tags 中是否有重复的标签
+    // 通过标签中的 slug_name 来去重
+    const repeatTag = recommend_tags.value.filter((tag) =>
+      reserved_tags.value.some((rTag) => rTag?.slug_name === tag?.slug_name),
+    );
+    if (repeatTag.length > 0) {
+      handleValueChange({
+        ...formData,
+        recommend_tags: {
+          ...recommend_tags,
+          errorMsg: t('recommend_tags.msg.contain_reserved'),
+          isInvalid: true,
+        },
+      });
+      bol = false;
+      const ele = document.getElementById('recommend_tags');
+      scrollToElementTop(ele);
+    } else {
+      handleValueChange({
+        ...formData,
+        recommend_tags: {
+          ...recommend_tags,
+          errorMsg: '',
+          isInvalid: false,
+        },
+      });
+    }
+    return bol;
+  };
+
+  const onSubmit = (evt) => {
+    evt.preventDefault();
+    evt.stopPropagation();
+    if (!checkValidated()) {
+      return;
+    }
+    const reqParams: Type.AdminTagsSetting = {
+      recommend_tags: formData.recommend_tags.value,
+      reserved_tags: formData.reserved_tags.value,
+      required_tag: formData.required_tag.value,
+    };
+    updateAdminTagsSetting(reqParams)
+      .then(() => {
+        Toast.onShow({
+          msg: t('update', { keyPrefix: 'toast' }),
+          variant: 'success',
+        });
+        writeSettingStore.getState().update({ ...reqParams });
+      })
+      .catch((err) => {
+        if (err.isError) {
+          const data = handleFormError(err, formData);
+          setFormData({ ...data });
+          const ele = document.getElementById(err.list[0].error_field);
+          scrollToElementTop(ele);
+        }
+      });
+  };
+
+  useEffect(() => {
+    getAdminTagsSetting().then((res) => {
+      if (res) {
+        const formMeta = { ...formData };
+        if (Array.isArray(res.recommend_tags)) {
+          formData.recommend_tags.value = res.recommend_tags;
+        } else {
+          formData.recommend_tags.value = [];
+        }
+        if (Array.isArray(res.reserved_tags)) {
+          formData.reserved_tags.value = res.reserved_tags;
+        } else {
+          formData.reserved_tags.value = [];
+        }
+        formMeta.required_tag.value = res.required_tag;
+        setFormData(formMeta);
+      }
+    });
+  }, []);
+
+  return (
+    <>
+      <h3 className="mb-4">{t('tags', { keyPrefix: 'nav_menus' })}</h3>
+      <TabNav menus={ADMIN_TAGS_NAV_MENUS} />
+      <div className="max-w-748">
+        <SchemaForm
+          schema={schema}
+          uiSchema={uiSchema}
+          formData={formData}
+          onChange={handleValueChange}
+          onSubmit={onSubmit}
+        />
+      </div>
+    </>
+  );
+};
+
+export default QaSettings;
diff --git a/ui/src/pages/Admin/Themes/index.tsx 
b/ui/src/pages/Admin/Themes/index.tsx
index 873dd7c9..c6983cbe 100644
--- a/ui/src/pages/Admin/Themes/index.tsx
+++ b/ui/src/pages/Admin/Themes/index.tsx
@@ -205,13 +205,15 @@ const Index: FC = () => {
   return (
     <>
       <h3 className="mb-4">{t('page_title')}</h3>
-      <SchemaForm
-        schema={schema}
-        formData={formData}
-        onSubmit={onSubmit}
-        uiSchema={uiSchema}
-        onChange={handleOnChange}
-      />
+      <div className="max-w-748">
+        <SchemaForm
+          schema={schema}
+          formData={formData}
+          onSubmit={onSubmit}
+          uiSchema={uiSchema}
+          onChange={handleOnChange}
+        />
+      </div>
     </>
   );
 };
diff --git a/ui/src/pages/Admin/Users/index.tsx 
b/ui/src/pages/Admin/Users/index.tsx
index 22042900..200aacf3 100644
--- a/ui/src/pages/Admin/Users/index.tsx
+++ b/ui/src/pages/Admin/Users/index.tsx
@@ -32,6 +32,7 @@ import {
   Empty,
   QueryGroup,
   Modal,
+  TabNav,
 } from '@/components';
 import * as Type from '@/common/interface';
 import { useUserModal } from '@/hooks';
@@ -45,6 +46,7 @@ import {
   deletePermanently,
 } from '@/services';
 import { formatCount } from '@/utils';
+import { ADMIN_USERS_NAV_MENUS } from '@/common/constants';
 
 import DeleteUserModal from './components/DeleteUserModal';
 import Action from './components/Action';
@@ -208,6 +210,7 @@ const Users: FC = () => {
   return (
     <>
       <h3 className="mb-4">{t('title')}</h3>
+      <TabNav menus={ADMIN_USERS_NAV_MENUS} />
       <div className="d-flex flex-wrap justify-content-between 
align-items-center">
         <Stack direction="horizontal" gap={3} className="mb-3">
           <QueryGroup
diff --git a/ui/src/pages/Admin/UsersSettings/index.tsx 
b/ui/src/pages/Admin/UsersSettings/index.tsx
new file mode 100644
index 00000000..769c9f0a
--- /dev/null
+++ b/ui/src/pages/Admin/UsersSettings/index.tsx
@@ -0,0 +1,132 @@
+import { useState, useEffect } from 'react';
+import { useTranslation } from 'react-i18next';
+
+import { SchemaForm, JSONSchema, UISchema, TabNav } from '@/components';
+import {
+  ADMIN_USERS_NAV_MENUS,
+  SYSTEM_AVATAR_OPTIONS,
+} from '@/common/constants';
+import { FormDataType } from '@/common/interface';
+import { useAdminUsersSettings, updateAdminUsersSettings } from '@/services';
+import { useToast } from '@/hooks';
+import { siteInfoStore } from '@/stores';
+import { handleFormError, scrollToElementTop } from '@/utils';
+
+const UsersSettings = () => {
+  const { t } = useTranslation('translation', {
+    keyPrefix: 'admin.interface',
+  });
+  const { data: setting } = useAdminUsersSettings();
+  const Toast = useToast();
+  const schema: JSONSchema = {
+    title: t('page_title'),
+    properties: {
+      default_avatar: {
+        type: 'string',
+        title: t('avatar.label'),
+        description: t('avatar.text'),
+        enum: SYSTEM_AVATAR_OPTIONS?.map((v) => v.value),
+        enumNames: SYSTEM_AVATAR_OPTIONS?.map((v) => v.label),
+        default: setting?.default_avatar || 'system',
+      },
+      gravatar_base_url: {
+        type: 'string',
+        title: t('gravatar_base_url.label'),
+        description: t('gravatar_base_url.text'),
+        default: setting?.gravatar_base_url || '',
+      },
+    },
+  };
+
+  const [formData, setFormData] = useState<FormDataType>({
+    default_avatar: {
+      value: setting?.default_avatar || 'system',
+      isInvalid: false,
+      errorMsg: '',
+    },
+    gravatar_base_url: {
+      value: setting?.gravatar_base_url || '',
+      isInvalid: false,
+      errorMsg: '',
+    },
+  });
+
+  const uiSchema: UISchema = {
+    default_avatar: {
+      'ui:widget': 'select',
+    },
+    gravatar_base_url: {
+      'ui:widget': 'input',
+      'ui:options': {
+        placeholder: 'https://www.gravatar.com/avatar/',
+      },
+    },
+  };
+
+  const handleValueChange = (data: FormDataType) => {
+    setFormData(data);
+  };
+
+  const onSubmit = (evt) => {
+    evt.preventDefault();
+    evt.stopPropagation();
+    const reqParams = {
+      default_avatar: formData.default_avatar.value,
+      gravatar_base_url: formData.gravatar_base_url.value,
+    };
+    updateAdminUsersSettings(reqParams)
+      .then(() => {
+        Toast.onShow({
+          msg: t('update', { keyPrefix: 'toast' }),
+          variant: 'success',
+        });
+        siteInfoStore.getState().updateUsers({
+          ...siteInfoStore.getState().users,
+          ...reqParams,
+        });
+      })
+      .catch((err) => {
+        if (err.isError) {
+          const data = handleFormError(err, formData);
+          setFormData({ ...data });
+          const ele = document.getElementById(err.list[0].error_field);
+          scrollToElementTop(ele);
+        }
+      });
+  };
+
+  useEffect(() => {
+    if (setting) {
+      const formMeta = {};
+      Object.keys(setting).forEach((k) => {
+        let v = setting[k];
+        if (k === 'default_avatar' && !v) {
+          v = 'system';
+        }
+        if (k === 'gravatar_base_url' && !v) {
+          v = '';
+        }
+        formMeta[k] = { ...formData[k], value: v };
+      });
+      setFormData({ ...formData, ...formMeta });
+    }
+  }, [setting]);
+
+  return (
+    <>
+      <h3 className="mb-4">{t('tags', { keyPrefix: 'nav_menus' })}</h3>
+      <TabNav menus={ADMIN_USERS_NAV_MENUS} />
+      <div className="max-w-748">
+        <SchemaForm
+          schema={schema}
+          uiSchema={uiSchema}
+          formData={formData}
+          onChange={handleValueChange}
+          onSubmit={onSubmit}
+        />
+      </div>
+    </>
+  );
+};
+
+export default UsersSettings;
diff --git a/ui/src/pages/Admin/Write/index.tsx 
b/ui/src/pages/Admin/Write/index.tsx
deleted file mode 100644
index fee2d28c..00000000
--- a/ui/src/pages/Admin/Write/index.tsx
+++ /dev/null
@@ -1,473 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { FC, useEffect, useState } from 'react';
-import { useTranslation } from 'react-i18next';
-import { Form, Button } from 'react-bootstrap';
-
-import { TagSelector } from '@/components';
-import type * as Type from '@/common/interface';
-import { useToast } from '@/hooks';
-import {
-  getRequireAndReservedTag,
-  postRequireAndReservedTag,
-} from '@/services';
-import { handleFormError, scrollToElementTop } from '@/utils';
-import { writeSettingStore } from '@/stores';
-
-const initFormData = {
-  reserved_tags: {
-    value: [] as Type.Tag[], // Replace `Type.Tag` with the correct type for 
`reserved_tags.value`
-    errorMsg: '',
-    isInvalid: false,
-  },
-  min_content: {
-    value: 0,
-    errorMsg: '',
-    isInvalid: false,
-  },
-  min_tags: {
-    value: 0,
-    errorMsg: '',
-    isInvalid: false,
-  },
-  recommend_tags: {
-    value: [] as Type.Tag[],
-    errorMsg: '',
-    isInvalid: false,
-  },
-  required_tag: {
-    value: false,
-    errorMsg: '',
-    isInvalid: false,
-  },
-  restrict_answer: {
-    value: false,
-    errorMsg: '',
-    isInvalid: false,
-  },
-  max_image_size: {
-    value: 0,
-    errorMsg: '',
-    isInvalid: false,
-  },
-  max_attachment_size: {
-    value: 0,
-    errorMsg: '',
-    isInvalid: false,
-  },
-  max_image_megapixel: {
-    value: 0,
-    errorMsg: '',
-    isInvalid: false,
-  },
-  authorized_image_extensions: {
-    value: '',
-    errorMsg: '',
-    isInvalid: false,
-  },
-  authorized_attachment_extensions: {
-    value: '',
-    errorMsg: '',
-    isInvalid: false,
-  },
-};
-
-const Index: FC = () => {
-  const { t } = useTranslation('translation', {
-    keyPrefix: 'admin.write',
-  });
-  const Toast = useToast();
-
-  const [formData, setFormData] = useState(initFormData);
-
-  const handleValueChange = (value) => {
-    setFormData({
-      ...formData,
-      ...value,
-    });
-  };
-
-  const checkValidated = (): boolean => {
-    let bol = true;
-    const { recommend_tags, reserved_tags } = formData;
-    // 找出 recommend_tags 和 reserved_tags 中是否有重复的标签
-    // 通过标签中的 slug_name 来去重
-    const repeatTag = recommend_tags.value.filter((tag) =>
-      reserved_tags.value.some((rTag) => rTag?.slug_name === tag?.slug_name),
-    );
-    if (repeatTag.length > 0) {
-      handleValueChange({
-        recommend_tags: {
-          ...recommend_tags,
-          errorMsg: t('recommend_tags.msg.contain_reserved'),
-          isInvalid: true,
-        },
-      });
-      bol = false;
-      const ele = document.getElementById('recommend_tags');
-      scrollToElementTop(ele);
-    } else {
-      handleValueChange({
-        recommend_tags: {
-          ...recommend_tags,
-          errorMsg: '',
-          isInvalid: false,
-        },
-      });
-    }
-    return bol;
-  };
-
-  const onSubmit = (evt) => {
-    evt.preventDefault();
-    evt.stopPropagation();
-    if (!checkValidated()) {
-      return;
-    }
-    const reqParams: Type.AdminSettingsWrite = {
-      recommend_tags: formData.recommend_tags.value,
-      min_tags: Number(formData.min_tags.value),
-      reserved_tags: formData.reserved_tags.value,
-      required_tag: formData.required_tag.value,
-      restrict_answer: formData.restrict_answer.value,
-      min_content: Number(formData.min_content.value),
-      max_image_size: Number(formData.max_image_size.value),
-      max_attachment_size: Number(formData.max_attachment_size.value),
-      max_image_megapixel: Number(formData.max_image_megapixel.value),
-      authorized_image_extensions:
-        formData.authorized_image_extensions.value?.length > 0
-          ? formData.authorized_image_extensions.value
-              .split(',')
-              ?.map((item) => item.trim().toLowerCase())
-          : [],
-      authorized_attachment_extensions:
-        formData.authorized_attachment_extensions.value?.length > 0
-          ? formData.authorized_attachment_extensions.value
-              .split(',')
-              ?.map((item) => item.trim().toLowerCase())
-          : [],
-    };
-    postRequireAndReservedTag(reqParams)
-      .then(() => {
-        Toast.onShow({
-          msg: t('update', { keyPrefix: 'toast' }),
-          variant: 'success',
-        });
-        writeSettingStore
-          .getState()
-          .update({ restrict_answer: reqParams.restrict_answer, ...reqParams 
});
-      })
-      .catch((err) => {
-        if (err.isError) {
-          const data = handleFormError(err, formData);
-          setFormData({ ...data });
-          const ele = document.getElementById(err.list[0].error_field);
-          scrollToElementTop(ele);
-        }
-      });
-  };
-
-  const initData = () => {
-    getRequireAndReservedTag().then((res) => {
-      if (Array.isArray(res.recommend_tags)) {
-        formData.recommend_tags.value = res.recommend_tags;
-      }
-      formData.min_content.value = res.min_content;
-      formData.min_tags.value = res.min_tags;
-      formData.required_tag.value = res.required_tag;
-      formData.restrict_answer.value = res.restrict_answer;
-      if (Array.isArray(res.reserved_tags)) {
-        formData.reserved_tags.value = res.reserved_tags;
-      }
-      formData.max_image_size.value = res.max_image_size;
-      formData.max_attachment_size.value = res.max_attachment_size;
-      formData.max_image_megapixel.value = res.max_image_megapixel;
-      formData.authorized_image_extensions.value =
-        res.authorized_image_extensions?.join(', ').toLowerCase();
-      formData.authorized_attachment_extensions.value =
-        res.authorized_attachment_extensions?.join(', ').toLowerCase();
-      setFormData({ ...formData });
-    });
-  };
-
-  useEffect(() => {
-    initData();
-  }, []);
-
-  return (
-    <>
-      <h3 className="mb-4">{t('page_title')}</h3>
-      <Form noValidate onSubmit={onSubmit}>
-        <Form.Group className="mb-3" controlId="reserved_tags">
-          <Form.Label>{t('reserved_tags.label')}</Form.Label>
-          <TagSelector
-            value={formData.reserved_tags.value}
-            onChange={(val) => {
-              handleValueChange({
-                reserved_tags: {
-                  value: val,
-                  errorMsg: '',
-                  isInvalid: false,
-                },
-              });
-            }}
-            showRequiredTag={false}
-            maxTagLength={0}
-            tagStyleMode="simple"
-            formText={t('reserved_tags.text')}
-            isInvalid={formData.reserved_tags.isInvalid}
-            errMsg={formData.reserved_tags.errorMsg}
-          />
-        </Form.Group>
-
-        <Form.Group className="mb-3" controlId="recommend_tags">
-          <Form.Label>{t('recommend_tags.label')}</Form.Label>
-          <TagSelector
-            value={formData.recommend_tags.value}
-            onChange={(val) => {
-              handleValueChange({
-                recommend_tags: {
-                  value: val,
-                  errorMsg: '',
-                  isInvalid: false,
-                },
-              });
-            }}
-            showRequiredTag={false}
-            tagStyleMode="simple"
-            formText={t('recommend_tags.text')}
-            isInvalid={formData.recommend_tags.isInvalid}
-            errMsg={formData.recommend_tags.errorMsg}
-          />
-        </Form.Group>
-        <Form.Group className="mb-3" controlId="min_tags">
-          <Form.Label>{t('min_tags.label')}</Form.Label>
-          <Form.Control
-            type="number"
-            inputMode="numeric"
-            min={0}
-            value={formData.min_tags.value}
-            isInvalid={formData.min_tags.isInvalid}
-            onChange={(evt) => {
-              handleValueChange({
-                min_tags: {
-                  value: evt.target.value,
-                  errorMsg: '',
-                  isInvalid: false,
-                },
-              });
-            }}
-          />
-          <Form.Text>{t('min_tags.text')}</Form.Text>
-          <Form.Control.Feedback type="invalid">
-            {formData.min_tags.errorMsg}
-          </Form.Control.Feedback>
-        </Form.Group>
-        <Form.Group className="mb-3" controlId="required_tag">
-          <Form.Label>{t('required_tag.title')}</Form.Label>
-          <Form.Switch
-            label={t('required_tag.label')}
-            checked={formData.required_tag.value}
-            onChange={(evt) => {
-              handleValueChange({
-                required_tag: {
-                  value: evt.target.checked,
-                  errorMsg: '',
-                  isInvalid: false,
-                },
-              });
-            }}
-          />
-          <Form.Text>{t('required_tag.text')}</Form.Text>
-          <Form.Control.Feedback type="invalid">
-            {formData.required_tag.errorMsg}
-          </Form.Control.Feedback>
-        </Form.Group>
-        <Form.Group className="mb-3" controlId="min_content">
-          <Form.Label>{t('min_content.label')}</Form.Label>
-          <Form.Control
-            type="number"
-            inputMode="numeric"
-            min={0}
-            value={formData.min_content.value}
-            isInvalid={formData.min_content.isInvalid}
-            onChange={(evt) => {
-              handleValueChange({
-                min_content: {
-                  value: evt.target.value,
-                  errorMsg: '',
-                  isInvalid: false,
-                },
-              });
-            }}
-          />
-          <Form.Text>{t('min_content.text')}</Form.Text>
-          <Form.Control.Feedback type="invalid">
-            {formData.min_content.errorMsg}
-          </Form.Control.Feedback>
-        </Form.Group>
-        <Form.Group className="mb-3" controlId="restrict_answer">
-          <Form.Label>{t('restrict_answer.title')}</Form.Label>
-          <Form.Switch
-            label={t('restrict_answer.label')}
-            checked={formData.restrict_answer.value}
-            onChange={(evt) => {
-              handleValueChange({
-                restrict_answer: {
-                  value: evt.target.checked,
-                  errorMsg: '',
-                  isInvalid: false,
-                },
-              });
-            }}
-          />
-          <Form.Text>{t('restrict_answer.text')}</Form.Text>
-          <Form.Control.Feedback type="invalid">
-            {formData.restrict_answer.errorMsg}
-          </Form.Control.Feedback>
-        </Form.Group>
-
-        <Form.Group className="mb-3" controlId="max_image_size">
-          <Form.Label>{t('image_size.label')}</Form.Label>
-          <Form.Control
-            type="number"
-            inputMode="numeric"
-            min={0}
-            value={formData.max_image_size.value}
-            isInvalid={formData.max_image_size.isInvalid}
-            onChange={(evt) => {
-              handleValueChange({
-                max_image_size: {
-                  value: evt.target.value,
-                  errorMsg: '',
-                  isInvalid: false,
-                },
-              });
-            }}
-          />
-          <Form.Text>{t('image_size.text')}</Form.Text>
-          <Form.Control.Feedback type="invalid">
-            {formData.max_image_size.errorMsg}
-          </Form.Control.Feedback>
-        </Form.Group>
-
-        <Form.Group className="mb-3" controlId="max_attachment_size">
-          <Form.Label>{t('attachment_size.label')}</Form.Label>
-          <Form.Control
-            type="number"
-            inputMode="numeric"
-            min={0}
-            value={formData.max_attachment_size.value}
-            isInvalid={formData.max_attachment_size.isInvalid}
-            onChange={(evt) => {
-              handleValueChange({
-                max_attachment_size: {
-                  value: evt.target.value,
-                  errorMsg: '',
-                  isInvalid: false,
-                },
-              });
-            }}
-          />
-          <Form.Text>{t('attachment_size.text')}</Form.Text>
-          <Form.Control.Feedback type="invalid">
-            {formData.max_attachment_size.errorMsg}
-          </Form.Control.Feedback>
-        </Form.Group>
-
-        <Form.Group className="mb-3" controlId="max_image_megapixel">
-          <Form.Label>{t('image_megapixels.label')}</Form.Label>
-          <Form.Control
-            type="number"
-            inputMode="numeric"
-            min={0}
-            isInvalid={formData.max_image_megapixel.isInvalid}
-            value={formData.max_image_megapixel.value}
-            onChange={(evt) => {
-              handleValueChange({
-                max_image_megapixel: {
-                  value: evt.target.value,
-                  errorMsg: '',
-                  isInvalid: false,
-                },
-              });
-            }}
-          />
-          <Form.Text>{t('image_megapixels.text')}</Form.Text>
-          <Form.Control.Feedback type="invalid">
-            {formData.max_image_megapixel.errorMsg}
-          </Form.Control.Feedback>
-        </Form.Group>
-
-        <Form.Group className="mb-3" controlId="authorized_image_extensions">
-          <Form.Label>{t('image_extensions.label')}</Form.Label>
-          <Form.Control
-            type="text"
-            value={formData.authorized_image_extensions.value}
-            isInvalid={formData.authorized_image_extensions.isInvalid}
-            onChange={(evt) => {
-              handleValueChange({
-                authorized_image_extensions: {
-                  value: evt.target.value.toLowerCase(),
-                  errorMsg: '',
-                  isInvalid: false,
-                },
-              });
-            }}
-          />
-          <Form.Text>{t('image_extensions.text')}</Form.Text>
-          <Form.Control.Feedback type="invalid">
-            {formData.authorized_image_extensions.errorMsg}
-          </Form.Control.Feedback>
-        </Form.Group>
-
-        <Form.Group
-          className="mb-3"
-          controlId="authorized_attachment_extensions">
-          <Form.Label>{t('attachment_extensions.label')}</Form.Label>
-          <Form.Control
-            type="text"
-            value={formData.authorized_attachment_extensions.value}
-            isInvalid={formData.authorized_attachment_extensions.isInvalid}
-            onChange={(evt) => {
-              handleValueChange({
-                authorized_attachment_extensions: {
-                  value: evt.target.value.toLowerCase(),
-                  errorMsg: '',
-                  isInvalid: false,
-                },
-              });
-            }}
-          />
-          <Form.Text>{t('attachment_extensions.text')}</Form.Text>
-          <Form.Control.Feedback type="invalid">
-            {formData.authorized_attachment_extensions.errorMsg}
-          </Form.Control.Feedback>
-        </Form.Group>
-
-        <Form.Group className="mb-3">
-          <Button type="submit">{t('save', { keyPrefix: 'btns' })}</Button>
-        </Form.Group>
-      </Form>
-    </>
-  );
-};
-
-export default Index;
diff --git a/ui/src/pages/Admin/index.scss b/ui/src/pages/Admin/index.scss
index 5748d053..c33e92be 100644
--- a/ui/src/pages/Admin/index.scss
+++ b/ui/src/pages/Admin/index.scss
@@ -25,6 +25,10 @@
   max-width: 30rem;
 }
 
+.max-w-748 {
+  max-width: 748px;
+}
+
 @media screen and (max-width: 768px) {
   .max-w-30 {
     max-width: 15rem;
diff --git a/ui/src/pages/Admin/index.tsx b/ui/src/pages/Admin/index.tsx
index 27503296..da167ac4 100644
--- a/ui/src/pages/Admin/index.tsx
+++ b/ui/src/pages/Admin/index.tsx
@@ -20,7 +20,7 @@
 import { FC } from 'react';
 import { useTranslation } from 'react-i18next';
 import { Row, Col } from 'react-bootstrap';
-import { Outlet, useMatch } from 'react-router-dom';
+import { Outlet } from 'react-router-dom';
 
 import { usePageTags } from '@/hooks';
 import { AdminSideNav, Footer } from '@/components';
@@ -28,19 +28,8 @@ import { AdminSideNav, Footer } from '@/components';
 import '@/common/sideNavLayout.scss';
 import './index.scss';
 
-const g10Paths = [
-  'dashboard',
-  'questions',
-  'answers',
-  'users',
-  'badges',
-  'flags',
-  'installed-plugins',
-];
 const Index: FC = () => {
   const { t } = useTranslation('translation', { keyPrefix: 'page_title' });
-  const pathMatch = useMatch('/admin/:path');
-  const curPath = pathMatch?.params.path || 'dashboard';
 
   usePageTags({
     title: t('admin'),
@@ -59,9 +48,6 @@ const Index: FC = () => {
               <Col className="page-main flex-auto">
                 <Outlet />
               </Col>
-              {g10Paths.find((v) => curPath === v) ? null : (
-                <Col className="page-right-side" />
-              )}
             </Row>
           </div>
         </div>
diff --git a/ui/src/pages/Layout/index.tsx b/ui/src/pages/Layout/index.tsx
index bd229165..048ca812 100644
--- a/ui/src/pages/Layout/index.tsx
+++ b/ui/src/pages/Layout/index.tsx
@@ -28,7 +28,7 @@ import {
   toastStore,
   loginToContinueStore,
   errorCodeStore,
-  siteLealStore,
+  siteSecurityStore,
   themeSettingStore,
 } from '@/stores';
 import {
@@ -49,7 +49,7 @@ const Layout: FC = () => {
   const location = useLocation();
   const { msg: toastMsg, variant, clear: toastClear } = toastStore();
   const externalToast = useExternalToast();
-  const externalContentDisplay = siteLealStore(
+  const externalContentDisplay = siteSecurityStore(
     (state) => state.external_content_display,
   );
   const closeToast = () => {
@@ -59,7 +59,6 @@ const Layout: FC = () => {
   const { show: showLoginToContinueModal } = loginToContinueStore();
   const { data: notificationData } = useQueryNotificationStatus();
   const layout = themeSettingStore((state) => state.layout);
-  console.log(layout);
   useEffect(() => {
     // handle footnote links
     const fixFootnoteLinks = () => {
diff --git a/ui/src/pages/Users/Settings/Profile/index.tsx 
b/ui/src/pages/Users/Settings/Profile/index.tsx
index 7fb8247c..62bd6fb8 100644
--- a/ui/src/pages/Users/Settings/Profile/index.tsx
+++ b/ui/src/pages/Users/Settings/Profile/index.tsx
@@ -25,7 +25,7 @@ import { sha256 } from 'js-sha256';
 
 import type { FormDataType } from '@/common/interface';
 import { UploadImg, Avatar, Icon, ImgViewer } from '@/components';
-import { loggedUserInfoStore, userCenterStore, interfaceStore } from 
'@/stores';
+import { loggedUserInfoStore, userCenterStore, siteInfoStore } from '@/stores';
 import { useToast } from '@/hooks';
 import {
   modifyUserInfo,
@@ -42,7 +42,7 @@ const Index: React.FC = () => {
   const toast = useToast();
   const { user, update } = loggedUserInfoStore();
   const { agent: ucAgent } = userCenterStore();
-  const { interface: interfaceSetting } = interfaceStore();
+  const { users: usersSettings } = siteInfoStore();
   const [mailHash, setMailHash] = useState('');
   const [count] = useState(0);
   const [profileAgent, setProfileAgent] = useState<UcSettingAgent>();
@@ -384,7 +384,7 @@ const Index: React.FC = () => {
                       <span>{t('avatar.gravatar_text')}</span>
                       <a
                         href={
-                          interfaceSetting.gravatar_base_url.includes(
+                          usersSettings.gravatar_base_url.includes(
                             'gravatar.cn',
                           )
                             ? 'https://gravatar.cn'
@@ -393,9 +393,7 @@ const Index: React.FC = () => {
                         className="ms-1"
                         target="_blank"
                         rel="noreferrer">
-                        {interfaceSetting.gravatar_base_url.includes(
-                          'gravatar.cn',
-                        )
+                        
{usersSettings.gravatar_base_url.includes('gravatar.cn')
                           ? 'gravatar.cn'
                           : 'gravatar.com'}
                       </a>
diff --git a/ui/src/router/routes.ts b/ui/src/router/routes.ts
index 3ca8431f..7d7003a1 100644
--- a/ui/src/router/routes.ts
+++ b/ui/src/router/routes.ts
@@ -358,9 +358,25 @@ const routes: RouteNode[] = [
             page: 'pages/Admin/Dashboard',
           },
           {
-            path: 'answers',
+            path: 'qa/questions',
+            page: 'pages/Admin/Questions',
+          },
+          {
+            path: 'qa/answers',
             page: 'pages/Admin/Answers',
           },
+          {
+            path: 'qa/settings',
+            page: 'pages/Admin/QaSettings',
+          },
+          {
+            path: 'tags/settings',
+            page: 'pages/Admin/TagsSettings',
+          },
+          {
+            path: 'security',
+            page: 'pages/Admin/Security',
+          },
           {
             path: 'themes',
             page: 'pages/Admin/Themes',
@@ -377,14 +393,14 @@ const routes: RouteNode[] = [
             path: 'interface',
             page: 'pages/Admin/Interface',
           },
-          {
-            path: 'questions',
-            page: 'pages/Admin/Questions',
-          },
           {
             path: 'users',
             page: 'pages/Admin/Users',
           },
+          {
+            path: 'users/settings',
+            page: 'pages/Admin/UsersSettings',
+          },
           {
             path: 'users/:user_id',
             page: 'pages/Admin/UserOverview',
@@ -398,12 +414,12 @@ const routes: RouteNode[] = [
             page: 'pages/Admin/Branding',
           },
           {
-            path: 'legal',
-            page: 'pages/Admin/Legal',
+            path: 'rules/policies',
+            page: 'pages/Admin/Policies',
           },
           {
-            path: 'write',
-            page: 'pages/Admin/Write',
+            path: 'files',
+            page: 'pages/Admin/Files',
           },
           {
             path: 'seo',
@@ -414,7 +430,7 @@ const routes: RouteNode[] = [
             page: 'pages/Admin/Login',
           },
           {
-            path: 'privileges',
+            path: 'rules/privileges',
             page: 'pages/Admin/Privileges',
           },
           {
diff --git a/ui/src/services/admin/index.ts b/ui/src/services/admin/index.ts
index af83d365..3fa211ee 100644
--- a/ui/src/services/admin/index.ts
+++ b/ui/src/services/admin/index.ts
@@ -25,3 +25,4 @@ export * from './users';
 export * from './dashboard';
 export * from './plugins';
 export * from './badges';
+export * from './tags';
diff --git a/ui/src/services/admin/question.ts 
b/ui/src/services/admin/question.ts
index 670534e2..bcd3ac7f 100644
--- a/ui/src/services/admin/question.ts
+++ b/ui/src/services/admin/question.ts
@@ -46,3 +46,13 @@ export const changeQuestionStatus = (
     status,
   });
 };
+
+export const getQuestionSetting = () => {
+  return request.get<Type.AdminQuestionSetting>(
+    '/answer/admin/api/siteinfo/question',
+  );
+};
+
+export const updateQuestionSetting = (params: Type.AdminQuestionSetting) => {
+  return request.put('/answer/admin/api/siteinfo/question', params);
+};
diff --git a/ui/src/services/admin/settings.ts 
b/ui/src/services/admin/settings.ts
index f2b9e598..c1f99d40 100644
--- a/ui/src/services/admin/settings.ts
+++ b/ui/src/services/admin/settings.ts
@@ -122,22 +122,12 @@ export const brandSetting = (params: 
Type.AdminSettingBranding) => {
   return request.put('/answer/admin/api/siteinfo/branding', params);
 };
 
-export const getRequireAndReservedTag = () => {
-  return request.get('/answer/admin/api/siteinfo/write');
+export const getAdminFilesSetting = () => {
+  return request.get('/answer/admin/api/siteinfo/advanced');
 };
 
-export const postRequireAndReservedTag = (params) => {
-  return request.put('/answer/admin/api/siteinfo/write', params);
-};
-
-export const getLegalSetting = () => {
-  return request.get<Type.AdminSettingsLegal>(
-    '/answer/admin/api/siteinfo/legal',
-  );
-};
-
-export const putLegalSetting = (params: Type.AdminSettingsLegal) => {
-  return request.put('/answer/admin/api/siteinfo/legal', params);
+export const updateAdminFilesSetting = (params: Type.AdminSettingsWrite) => {
+  return request.put('/answer/admin/api/siteinfo/advanced', params);
 };
 
 export const getSeoSetting = () => {
@@ -195,3 +185,23 @@ export const getPrivilegeSetting = () => {
 export const putPrivilegeSetting = (params: AdminSettingsPrivilegeReq) => {
   return request.put('/answer/admin/api/setting/privileges', params);
 };
+
+export const getPoliciesSetting = () => {
+  return request.get<Type.AdminSettingsLegal>(
+    '/answer/admin/api/siteinfo/polices',
+  );
+};
+
+export const putPoliciesSetting = (params: Type.AdminSettingsLegal) => {
+  return request.put('/answer/admin/api/siteinfo/polices', params);
+};
+
+export const getSecuritySetting = () => {
+  return request.get<Type.AdminSettingsSecurity>(
+    '/answer/admin/api/siteinfo/security',
+  );
+};
+
+export const putSecuritySetting = (params: Type.AdminSettingsSecurity) => {
+  return request.put('/answer/admin/api/siteinfo/security', params);
+};
diff --git a/ui/src/pages/Admin/index.scss b/ui/src/services/admin/tags.ts
similarity index 69%
copy from ui/src/pages/Admin/index.scss
copy to ui/src/services/admin/tags.ts
index 5748d053..c7d6bc77 100644
--- a/ui/src/pages/Admin/index.scss
+++ b/ui/src/services/admin/tags.ts
@@ -17,20 +17,13 @@
  * under the License.
  */
 
-.min-w-15 {
-  min-width: 15rem;
-}
+import request from '@/utils/request';
+import type * as Type from '@/common/interface';
 
-.max-w-30 {
-  max-width: 30rem;
-}
+export const getAdminTagsSetting = () => {
+  return request.get<Type.AdminTagsSetting>('/answer/admin/api/siteinfo/tag');
+};
 
-@media screen and (max-width: 768px) {
-  .max-w-30 {
-    max-width: 15rem;
-  }
-}
-
-.table tr th {
-  white-space: nowrap;
-}
+export const updateAdminTagsSetting = (params: Type.AdminTagsSetting) => {
+  return request.put('/answer/admin/api/siteinfo/tag', params);
+};
diff --git a/ui/src/services/admin/users.ts b/ui/src/services/admin/users.ts
index ee8cad5d..c7aeafe5 100644
--- a/ui/src/services/admin/users.ts
+++ b/ui/src/services/admin/users.ts
@@ -94,3 +94,22 @@ export const postUserActivation = (userId: string) => {
     user_id: userId,
   });
 };
+
+export const useAdminUsersSettings = () => {
+  const apiUrl = `/answer/admin/api/siteinfo/users-settings`;
+  const { data, error } = useSWR<
+    {
+      default_avatar: string;
+      gravatar_base_url: string;
+    },
+    Error
+  >(apiUrl, request.instance.get);
+  return { data, isLoading: !data && !error, error };
+};
+
+export const updateAdminUsersSettings = (params: {
+  default_avatar: string;
+  gravatar_base_url: string;
+}) => {
+  return request.put('/answer/admin/api/siteinfo/users-settings', params);
+};
diff --git a/ui/src/stores/index.ts b/ui/src/stores/index.ts
index 66d59b32..1bd2fece 100644
--- a/ui/src/stores/index.ts
+++ b/ui/src/stores/index.ts
@@ -33,7 +33,7 @@ import loginToContinueStore from './loginToContinue';
 import errorCodeStore from './errorCode';
 import sideNavStore from './sideNav';
 import commentReplyStore from './commentReply';
-import siteLealStore from './siteLegal';
+import siteSecurityStore from './siteSecurity';
 
 export {
   toastStore,
@@ -52,5 +52,5 @@ export {
   sideNavStore,
   commentReplyStore,
   writeSettingStore,
-  siteLealStore,
+  siteSecurityStore,
 };
diff --git a/ui/src/stores/interface.ts b/ui/src/stores/interface.ts
index cceb425c..fdb4ab96 100644
--- a/ui/src/stores/interface.ts
+++ b/ui/src/stores/interface.ts
@@ -35,9 +35,9 @@ const interfaceSetting = create<InterfaceType>((set) => ({
     gravatar_base_url: '',
   },
   update: (params) =>
-    set(() => {
+    set((state) => {
       return {
-        interface: params,
+        interface: { ...state.interface, ...params },
       };
     }),
 }));
diff --git a/ui/src/stores/loginSetting.ts b/ui/src/stores/loginSetting.ts
index 73fd4880..7acf765e 100644
--- a/ui/src/stores/loginSetting.ts
+++ b/ui/src/stores/loginSetting.ts
@@ -29,7 +29,6 @@ interface IType {
 const loginSetting = create<IType>((set) => ({
   login: {
     allow_new_registrations: true,
-    login_required: false,
     allow_email_registrations: true,
     allow_email_domains: [],
     allow_password_login: true,
diff --git a/ui/src/stores/siteInfo.ts b/ui/src/stores/siteInfo.ts
index 725546d6..c529cc62 100644
--- a/ui/src/stores/siteInfo.ts
+++ b/ui/src/stores/siteInfo.ts
@@ -39,6 +39,8 @@ const defaultUsersConf: AdminSettingsUsers = {
   allow_update_location: false,
   allow_update_username: false,
   allow_update_website: false,
+  default_avatar: 'system',
+  gravatar_base_url: '',
 };
 
 const siteInfo = create<SiteInfoType>((set) => ({
@@ -48,7 +50,6 @@ const siteInfo = create<SiteInfoType>((set) => ({
     short_description: '',
     site_url: '',
     contact_email: '',
-    check_update: true,
     permalink: 1,
   },
   users: defaultUsersConf,
diff --git a/ui/src/stores/siteLegal.ts b/ui/src/stores/siteSecurity.ts
similarity index 75%
rename from ui/src/stores/siteLegal.ts
rename to ui/src/stores/siteSecurity.ts
index 29a26ea3..e4e5bb52 100644
--- a/ui/src/stores/siteLegal.ts
+++ b/ui/src/stores/siteSecurity.ts
@@ -19,12 +19,20 @@
 
 import { create } from 'zustand';
 
-interface LealStore {
+interface SecurityStore {
+  login_required: boolean;
+  check_update: boolean;
   external_content_display: string;
-  update: (params: { external_content_display: string }) => void;
+  update: (params: {
+    external_content_display: string;
+    check_update: boolean;
+    login_required: boolean;
+  }) => void;
 }
 
-const siteLealStore = create<LealStore>((set) => ({
+const siteSecurityStore = create<SecurityStore>((set) => ({
+  login_required: false,
+  check_update: true,
   external_content_display: 'always_display',
   update: (params) =>
     set((state) => {
@@ -35,4 +43,4 @@ const siteLealStore = create<LealStore>((set) => ({
     }),
 }));
 
-export default siteLealStore;
+export default siteSecurityStore;
diff --git a/ui/src/stores/writeSetting.ts b/ui/src/stores/writeSetting.ts
index 9f6542d2..576979a2 100644
--- a/ui/src/stores/writeSetting.ts
+++ b/ui/src/stores/writeSetting.ts
@@ -19,11 +19,17 @@
 
 import { create } from 'zustand';
 
-import { AdminSettingsWrite } from '@/common/interface';
+import {
+  AdminSettingsWrite,
+  AdminQuestionSetting,
+  AdminTagsSetting,
+} from '@/common/interface';
 
 interface IProps {
-  write: AdminSettingsWrite;
-  update: (params: AdminSettingsWrite) => void;
+  write: AdminSettingsWrite & AdminQuestionSetting & AdminTagsSetting;
+  update: (
+    params: AdminSettingsWrite | AdminQuestionSetting | AdminTagsSetting,
+  ) => void;
 }
 
 const Index = create<IProps>((set) => ({
diff --git a/ui/src/utils/guard.ts b/ui/src/utils/guard.ts
index 24064669..6e6a6d80 100644
--- a/ui/src/utils/guard.ts
+++ b/ui/src/utils/guard.ts
@@ -30,7 +30,7 @@ import {
   loginToContinueStore,
   pageTagStore,
   writeSettingStore,
-  siteLealStore,
+  siteSecurityStore,
 } from '@/stores';
 import { RouteAlias } from '@/router/alias';
 import {
@@ -263,8 +263,8 @@ export const singUpAgent = () => {
 
 export const shouldLoginRequired = () => {
   const gr: TGuardResult = { ok: true };
-  const loginSetting = loginSettingStore.getState().login;
-  if (!loginSetting.login_required) {
+  const { login_required } = siteSecurityStore.getState();
+  if (!login_required) {
     return gr;
   }
   const us = deriveLoginState();
@@ -382,12 +382,11 @@ export const initAppSettingsStore = async () => {
     themeSettingStore.getState().update(appSettings.theme);
     seoSettingStore.getState().update(appSettings.site_seo);
     writeSettingStore.getState().update({
-      restrict_answer: appSettings.site_write.restrict_answer,
-      ...appSettings.site_write,
-    });
-    siteLealStore.getState().update({
-      external_content_display: 
appSettings.site_legal.external_content_display,
+      ...appSettings.site_advanced,
+      ...appSettings.site_questions,
+      ...appSettings.site_tags,
     });
+    siteSecurityStore.getState().update(appSettings.site_security);
   }
 };
 

Reply via email to