You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@answer.apache.org by sh...@apache.org on 2025/06/26 08:45:16 UTC

(answer) branch feat/1.6.1/md updated (a963f5a8 -> 17e1b301)

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

shuai pushed a change to branch feat/1.6.1/md
in repository https://gitbox.apache.org/repos/asf/answer.git


    from a963f5a8 fix: hr tag's margin use 1.5rem
     new 19eca49d fix: move the settings -> users section in admin to Interface #1360
     new 17e1b301 feat: Blocked users can choose the duration of the block #1361

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


Summary of changes:
 i18n/en_US.yaml                                    |  12 ++
 i18n/zh_CN.yaml                                    |  12 ++
 ui/src/common/constants.ts                         |  54 ++++-
 ui/src/common/interface.ts                         |   4 +-
 ui/src/components/Header/index.tsx                 |   1 -
 ui/src/pages/Admin/Interface/index.tsx             |  51 ++++-
 ui/src/pages/Admin/SettingsUsers/index.tsx         | 218 ---------------------
 .../pages/Admin/Users/components/Action/index.tsx  |  16 +-
 .../Users/components/SuspenseUserModal/index.tsx   |  90 +++++++++
 ui/src/pages/Admin/Users/index.tsx                 |  54 ++++-
 ui/src/pages/Users/Settings/Profile/index.tsx      |  24 +--
 ui/src/router/routes.ts                            |   4 -
 ui/src/services/admin/settings.ts                  |   2 -
 ui/src/stores/interface.ts                         |   1 +
 ui/src/stores/siteInfo.ts                          |   2 -
 15 files changed, 279 insertions(+), 266 deletions(-)
 delete mode 100644 ui/src/pages/Admin/SettingsUsers/index.tsx
 create mode 100644 ui/src/pages/Admin/Users/components/SuspenseUserModal/index.tsx


(answer) 02/02: feat: Blocked users can choose the duration of the block #1361

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

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

commit 17e1b301752cd136e98cf83e81bae89cd03a3007
Author: shuai <li...@sifou.com>
AuthorDate: Thu Jun 26 16:44:42 2025 +0800

    feat: Blocked users can choose the duration of the block #1361
---
 i18n/en_US.yaml                                    | 12 +++
 i18n/zh_CN.yaml                                    | 12 +++
 ui/src/common/constants.ts                         | 30 +++++---
 ui/src/components/Header/index.tsx                 |  1 -
 .../pages/Admin/Users/components/Action/index.tsx  | 16 ++--
 .../Users/components/SuspenseUserModal/index.tsx   | 90 ++++++++++++++++++++++
 ui/src/pages/Admin/Users/index.tsx                 | 13 +++-
 7 files changed, 150 insertions(+), 24 deletions(-)

diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml
index cb05e5e1..3889a12a 100644
--- a/i18n/en_US.yaml
+++ b/i18n/en_US.yaml
@@ -1070,6 +1070,9 @@ ui:
     day: day
     hours: hours
     days: days
+    month: month
+    months: months
+    year: year
   reaction:
     heart: heart
     smile: smile
@@ -1930,6 +1933,7 @@ ui:
       created_at: Created time
       delete_at: Deleted time
       suspend_at: Suspended time
+      suspend_until: Suspend until
       status: Status
       role: Role
       action: Action
@@ -1964,6 +1968,8 @@ ui:
       suspend_user:
         title: Suspend this user
         content: A suspended user can't log in.
+        label: How long will the user be suspended for?
+        forever: Forever
     questions:
       page_title: Questions
       unlisted: Unlisted
@@ -2024,6 +2030,12 @@ ui:
         label: Timezone
         msg: Timezone cannot be empty.
         text: Choose a city in the same timezone as you.
+      avatar:
+        label: Default avatar
+        text: For users without a custom avatar of their own.
+      gravatar_base_url:
+        label: Gravatar base URL
+        text: URL of the Gravatar provider's API base. Ignored when empty.
     smtp:
       page_title: SMTP
       from_email:
diff --git a/i18n/zh_CN.yaml b/i18n/zh_CN.yaml
index 9852ba60..ce5fbb07 100644
--- a/i18n/zh_CN.yaml
+++ b/i18n/zh_CN.yaml
@@ -1056,6 +1056,9 @@ ui:
     day: 天
     hours: 小时
     days: 日
+    month: 月
+    months: 月
+    year: 年
   reaction:
     heart: 爱心
     smile: 微笑
@@ -1890,6 +1893,7 @@ ui:
       created_at: 创建时间
       delete_at: 删除时间
       suspend_at: 封禁时间
+      suspend_until: 封禁到期
       status: 状态
       role: 角色
       action: 操作
@@ -1924,6 +1928,8 @@ ui:
       suspend_user:
         title: 挂起此用户
         content: 被封禁的用户将无法登录。
+        label: 用户将被封禁多长时间?
+        forever: 永久
     questions:
       page_title: 问题
       unlisted: 已隐藏
@@ -1984,6 +1990,12 @@ ui:
         label: 时区
         msg: 时区不能为空。
         text: 选择一个与您相同时区的城市。
+      avatar:
+        label: 默认头像
+        text: 没有自定义头像的用户。
+      gravatar_base_url:
+        label: Gravatar 根路径 URL
+        text: Gravatar 提供商的 API 基础的 URL。当为空时忽略。
     smtp:
       page_title: SMTP
       from_email:
diff --git a/ui/src/common/constants.ts b/ui/src/common/constants.ts
index 7ca57400..18f25114 100644
--- a/ui/src/common/constants.ts
+++ b/ui/src/common/constants.ts
@@ -663,42 +663,52 @@ export const DEFAULT_THEME_COLOR = '#0033ff';
 export const SUSPENSE_USER_TIME = [
   {
     label: 'hours',
-    value: '24',
+    time: '24',
+    value: '24h',
   },
   {
     label: 'hours',
-    value: '48',
+    time: '48',
+    value: '48h',
   },
   {
     label: 'hours',
-    value: '72',
+    time: '72',
+    value: '72h',
   },
   {
     label: 'days',
-    value: '7',
+    time: '7',
+    value: '7d',
   },
   {
     label: 'days',
-    value: '14',
+    time: '14',
+    value: '14d',
   },
   {
     label: 'months',
-    value: '1',
+    time: '1',
+    value: '1m',
   },
   {
     label: 'months',
-    value: '2',
+    time: '2',
+    value: '2m',
   },
   {
     label: 'months',
-    value: '3',
+    time: '3',
+    value: '3m',
   },
   {
     label: 'months',
-    value: '6',
+    time: '6',
+    value: '6m',
   },
   {
     label: 'year',
-    value: '1',
+    time: '1',
+    value: '1y',
   },
 ];
diff --git a/ui/src/components/Header/index.tsx b/ui/src/components/Header/index.tsx
index 0b015ba6..dbc7565d 100644
--- a/ui/src/components/Header/index.tsx
+++ b/ui/src/components/Header/index.tsx
@@ -87,7 +87,6 @@ const Header: FC = () => {
   if (theme_config?.[theme]?.navbar_style) {
     // const color = theme_config[theme].navbar_style.startsWith('#')
     themeMode = isLight(theme_config[theme].navbar_style) ? 'light' : 'dark';
-    console.log('isLightTheme', themeMode);
     navbarStyle = `theme-${themeMode}`;
   }
 
diff --git a/ui/src/pages/Admin/Users/components/Action/index.tsx b/ui/src/pages/Admin/Users/components/Action/index.tsx
index 9f0847a2..b55971f4 100644
--- a/ui/src/pages/Admin/Users/components/Action/index.tsx
+++ b/ui/src/pages/Admin/Users/components/Action/index.tsx
@@ -42,6 +42,7 @@ interface Props {
   currentUser;
   refreshUsers: () => void;
   showDeleteModal: (val) => void;
+  showSuspenseModal: (val) => void;
   userData;
 }
 
@@ -53,6 +54,7 @@ const UserOperation = ({
   refreshUsers,
   showDeleteModal,
   userData,
+  showSuspenseModal,
 }: Props) => {
   const { t } = useTranslation('translation', { keyPrefix: 'admin.users' });
   const Toast = useToast();
@@ -170,17 +172,9 @@ const UserOperation = ({
 
     if (type === 'suspend') {
       // cons
-      Modal.confirm({
-        title: t('suspend_user.title'),
-        content: t('suspend_user.content'),
-        cancelBtnVariant: 'link',
-        cancelText: t('cancel', { keyPrefix: 'btns' }),
-        confirmBtnVariant: 'danger',
-        confirmText: t('suspend', { keyPrefix: 'btns' }),
-        onConfirm: () => {
-          // active -> suspended
-          postUserStatus('suspended');
-        },
+      showSuspenseModal({
+        show: true,
+        userId: userData.user_id,
       });
     }
 
diff --git a/ui/src/pages/Admin/Users/components/SuspenseUserModal/index.tsx b/ui/src/pages/Admin/Users/components/SuspenseUserModal/index.tsx
new file mode 100644
index 00000000..dc59ff34
--- /dev/null
+++ b/ui/src/pages/Admin/Users/components/SuspenseUserModal/index.tsx
@@ -0,0 +1,90 @@
+/*
+ * 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 { useState } from 'react';
+import { Modal, Button, Form } from 'react-bootstrap';
+import { useTranslation } from 'react-i18next';
+
+import { changeUserStatus } from '@/services';
+import { SUSPENSE_USER_TIME } from '@/common/constants';
+import { toastStore } from '@/stores';
+
+const SuspenseUserModal = ({ show, userId, onClose, refreshUsers }) => {
+  const { t } = useTranslation('translation', { keyPrefix: 'admin.users' });
+  const [checkVal, setCheckVal] = useState('forever');
+
+  const handleClose = () => {
+    onClose();
+    setCheckVal('forever');
+  };
+
+  const handleSubmit = (e) => {
+    e.preventDefault();
+    changeUserStatus({
+      user_id: userId,
+      status: 'suspended',
+      suspend_duration: checkVal,
+    }).then(() => {
+      toastStore.getState().show({
+        msg: t('user_suspended', { keyPrefix: 'messages' }),
+        variant: 'success',
+      });
+      refreshUsers?.();
+      handleClose();
+    });
+  };
+
+  return (
+    <Modal show={show} onHide={handleClose}>
+      <Modal.Header closeButton>
+        <Modal.Title>{t('suspend_user.title')}</Modal.Title>
+      </Modal.Header>
+      <Modal.Body>
+        <p>{t('suspend_user.content')}</p>
+        <Form>
+          <Form.Group controlId="delete_user" className="mb-3">
+            <Form.Label>{t('suspend_user.label')}</Form.Label>
+            <Form.Select
+              value={checkVal}
+              onChange={(e) => setCheckVal(e.target.value)}>
+              <option value="forever">{t('suspend_user.forever')}</option>
+              {SUSPENSE_USER_TIME.map((item) => {
+                return (
+                  <option key={item.value} value={item.value}>
+                    {item.time} {t(item.label, { keyPrefix: 'dates' })}
+                  </option>
+                );
+              })}
+            </Form.Select>
+          </Form.Group>
+        </Form>
+      </Modal.Body>
+      <Modal.Footer>
+        <Button variant="link" onClick={handleClose}>
+          {t('cancel', { keyPrefix: 'btns' })}
+        </Button>
+        <Button variant="danger" onClick={handleSubmit}>
+          {t('suspend', { keyPrefix: 'btns' })}
+        </Button>
+      </Modal.Footer>
+    </Modal>
+  );
+};
+
+export default SuspenseUserModal;
diff --git a/ui/src/pages/Admin/Users/index.tsx b/ui/src/pages/Admin/Users/index.tsx
index 295cb075..7e8e4fd6 100644
--- a/ui/src/pages/Admin/Users/index.tsx
+++ b/ui/src/pages/Admin/Users/index.tsx
@@ -23,6 +23,7 @@ import { useSearchParams } from 'react-router-dom';
 import { useTranslation } from 'react-i18next';
 
 import classNames from 'classnames';
+import dayjs from 'dayjs';
 
 import {
   Pagination,
@@ -297,7 +298,14 @@ const Users: FC = () => {
                       <FormatTime time={user.suspended_at} />
                     </td>
                     <td className="text-nowrap">
-                      <FormatTime time={user.suspended_at} />
+                      {user.suspended_until <= 0 ||
+                      Number(
+                        dayjs(user.suspended_until * 1000).format('YYYY'),
+                      ) > 2099
+                        ? t('suspend_user.forever')
+                        : dayjs(user.suspended_until * 1000).format(
+                            t('long_date_with_time', { keyPrefix: 'dates' }),
+                          )}
                     </td>
                   </>
                 )}
@@ -357,13 +365,14 @@ const Users: FC = () => {
       />
       <SuspenseUserModal
         show={suspenseUserModalState.show}
+        userId={suspenseUserModalState.userId}
         onClose={() => {
           handleSuspenseUserModalState({
             show: false,
             userId: '',
           });
         }}
-        onDelete={(val) => handleDelete(val)}
+        refreshUsers={refreshUsers}
       />
     </>
   );


(answer) 01/02: fix: move the settings -> users section in admin to Interface #1360

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

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

commit 19eca49d2a8e1a6b6fa67268061567421a2fcd04
Author: shuai <li...@sifou.com>
AuthorDate: Thu Jun 26 14:55:35 2025 +0800

    fix: move the settings -> users section in admin to Interface #1360
---
 ui/src/common/constants.ts                    |  44 +++++-
 ui/src/common/interface.ts                    |   4 +-
 ui/src/pages/Admin/Interface/index.tsx        |  51 +++++-
 ui/src/pages/Admin/SettingsUsers/index.tsx    | 218 --------------------------
 ui/src/pages/Admin/Users/index.tsx            |  45 +++++-
 ui/src/pages/Users/Settings/Profile/index.tsx |  24 ++-
 ui/src/router/routes.ts                       |   4 -
 ui/src/services/admin/settings.ts             |   2 -
 ui/src/stores/interface.ts                    |   1 +
 ui/src/stores/siteInfo.ts                     |   2 -
 10 files changed, 141 insertions(+), 254 deletions(-)

diff --git a/ui/src/common/constants.ts b/ui/src/common/constants.ts
index 163d4989..7ca57400 100644
--- a/ui/src/common/constants.ts
+++ b/ui/src/common/constants.ts
@@ -126,7 +126,6 @@ export const ADMIN_NAV_MENUS = [
       { name: 'write' },
       { name: 'seo' },
       { name: 'login' },
-      { name: 'users', path: 'settings-users' },
       { name: 'privileges' },
     ],
   },
@@ -660,3 +659,46 @@ export const SYSTEM_AVATAR_OPTIONS = [
 export const TAG_SLUG_NAME_MAX_LENGTH = 35;
 
 export const DEFAULT_THEME_COLOR = '#0033ff';
+
+export const SUSPENSE_USER_TIME = [
+  {
+    label: 'hours',
+    value: '24',
+  },
+  {
+    label: 'hours',
+    value: '48',
+  },
+  {
+    label: 'hours',
+    value: '72',
+  },
+  {
+    label: 'days',
+    value: '7',
+  },
+  {
+    label: 'days',
+    value: '14',
+  },
+  {
+    label: 'months',
+    value: '1',
+  },
+  {
+    label: 'months',
+    value: '2',
+  },
+  {
+    label: 'months',
+    value: '3',
+  },
+  {
+    label: 'months',
+    value: '6',
+  },
+  {
+    label: 'year',
+    value: '1',
+  },
+];
diff --git a/ui/src/common/interface.ts b/ui/src/common/interface.ts
index 3935c439..114b0e4c 100644
--- a/ui/src/common/interface.ts
+++ b/ui/src/common/interface.ts
@@ -382,6 +382,8 @@ 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 {
@@ -403,8 +405,6 @@ export interface AdminSettingsUsers {
   allow_update_location: boolean;
   allow_update_username: boolean;
   allow_update_website: boolean;
-  default_avatar: string;
-  gravatar_base_url: string;
 }
 
 export interface SiteSettings {
diff --git a/ui/src/pages/Admin/Interface/index.tsx b/ui/src/pages/Admin/Interface/index.tsx
index 9c622048..865029e9 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 } from '@/common/constants';
+import { DEFAULT_TIMEZONE, SYSTEM_AVATAR_OPTIONS } from '@/common/constants';
 import {
   updateInterfaceSetting,
   useInterfaceSetting,
@@ -59,7 +59,8 @@ const Interface: FC = () => {
         description: t('language.text'),
         enum: langs?.map((lang) => lang.value),
         enumNames: langs?.map((lang) => lang.label),
-        default: setting?.language || storeInterface.language,
+        default:
+          setting?.language || storeInterface.language || langs?.[0]?.value,
       },
       time_zone: {
         type: 'string',
@@ -67,12 +68,26 @@ 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 || '',
+      },
     },
   };
 
   const [formData, setFormData] = useState<FormDataType>({
     language: {
-      value: setting?.language || storeInterface.language,
+      value: setting?.language || storeInterface.language || langs?.[0]?.value,
       isInvalid: false,
       errorMsg: '',
     },
@@ -81,6 +96,16 @@ 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 = {
@@ -90,6 +115,15 @@ 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);
@@ -122,6 +156,8 @@ 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)
@@ -151,7 +187,14 @@ const Interface: FC = () => {
     if (setting) {
       const formMeta = {};
       Object.keys(setting).forEach((k) => {
-        formMeta[k] = { ...formData[k], value: setting[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 });
     }
diff --git a/ui/src/pages/Admin/SettingsUsers/index.tsx b/ui/src/pages/Admin/SettingsUsers/index.tsx
deleted file mode 100644
index e0980980..00000000
--- a/ui/src/pages/Admin/SettingsUsers/index.tsx
+++ /dev/null
@@ -1,218 +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, FormEvent, useEffect, useState } from 'react';
-import { useTranslation } from 'react-i18next';
-
-import { useToast } from '@/hooks';
-import { FormDataType } from '@/common/interface';
-import { JSONSchema, SchemaForm, UISchema, initFormData } from '@/components';
-import { SYSTEM_AVATAR_OPTIONS } from '@/common/constants';
-import {
-  getUsersSetting,
-  putUsersSetting,
-  AdminSettingsUsers,
-} from '@/services';
-import { handleFormError, scrollToElementTop } from '@/utils';
-import * as Type from '@/common/interface';
-import { siteInfoStore } from '@/stores';
-
-const Index: FC = () => {
-  const { t } = useTranslation('translation', {
-    keyPrefix: 'admin.settings_users',
-  });
-  const Toast = useToast();
-  const { updateUsers: updateUsersStore } = siteInfoStore();
-  const schema: JSONSchema = {
-    title: t('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: 'system',
-      },
-      gravatar_base_url: {
-        type: 'string',
-        title: t('gravatar_base_url.label'),
-        description: t('gravatar_base_url.text'),
-      },
-      profile_editable: {
-        type: 'string',
-        title: t('profile_editable.title'),
-      },
-      allow_update_display_name: {
-        type: 'boolean',
-        title: 'allow_update_display_name',
-      },
-      allow_update_username: {
-        type: 'boolean',
-        title: 'allow_update_username',
-      },
-      allow_update_avatar: {
-        type: 'boolean',
-        title: 'allow_update_avatar',
-      },
-      allow_update_bio: {
-        type: 'boolean',
-        title: 'allow_update_bio',
-      },
-      allow_update_website: {
-        type: 'boolean',
-        title: 'allow_update_website',
-      },
-      allow_update_location: {
-        type: 'boolean',
-        title: 'allow_update_location',
-      },
-    },
-  };
-
-  const [formData, setFormData] = useState<FormDataType>(initFormData(schema));
-
-  const uiSchema: UISchema = {
-    default_avatar: {
-      'ui:widget': 'select',
-    },
-    gravatar_base_url: {
-      'ui:widget': 'input',
-      'ui:options': {
-        placeholder: 'https://www.gravatar.com/avatar/',
-      },
-    },
-    profile_editable: {
-      'ui:widget': 'legend',
-    },
-    allow_update_display_name: {
-      'ui:widget': 'switch',
-      'ui:options': {
-        label: t('allow_update_display_name.label'),
-        simplify: true,
-      },
-    },
-    allow_update_username: {
-      'ui:widget': 'switch',
-      'ui:options': {
-        label: t('allow_update_username.label'),
-        simplify: true,
-      },
-    },
-    allow_update_avatar: {
-      'ui:widget': 'switch',
-      'ui:options': {
-        label: t('allow_update_avatar.label'),
-        simplify: true,
-      },
-    },
-    allow_update_bio: {
-      'ui:widget': 'switch',
-      'ui:options': {
-        label: t('allow_update_bio.label'),
-        simplify: true,
-      },
-    },
-    allow_update_website: {
-      'ui:widget': 'switch',
-      'ui:options': {
-        label: t('allow_update_website.label'),
-        simplify: true,
-      },
-    },
-    allow_update_location: {
-      'ui:widget': 'switch',
-      'ui:options': {
-        label: t('allow_update_location.label'),
-        field_class_name: 'mb-3',
-        simplify: true,
-      },
-    },
-  };
-
-  const onSubmit = (evt: FormEvent) => {
-    evt.preventDefault();
-    evt.stopPropagation();
-    const reqParams: AdminSettingsUsers = {
-      allow_update_avatar: formData.allow_update_avatar.value,
-      allow_update_bio: formData.allow_update_bio.value,
-      allow_update_display_name: formData.allow_update_display_name.value,
-      allow_update_location: formData.allow_update_location.value,
-      allow_update_username: formData.allow_update_username.value,
-      allow_update_website: formData.allow_update_website.value,
-      default_avatar: formData.default_avatar.value,
-      gravatar_base_url: formData.gravatar_base_url.value,
-    };
-    putUsersSetting(reqParams)
-      .then(() => {
-        updateUsersStore(reqParams);
-        Toast.onShow({
-          msg: t('update', { keyPrefix: 'toast' }),
-          variant: 'success',
-        });
-      })
-      .catch((err) => {
-        if (err.isError) {
-          const data = handleFormError(err, formData);
-          setFormData({ ...data });
-          const ele = document.getElementById(err.list[0].error_field);
-          scrollToElementTop(ele);
-        }
-      });
-  };
-
-  useEffect(() => {
-    getUsersSetting().then((resp) => {
-      if (!resp) {
-        return;
-      }
-      const formMeta: Type.FormDataType = {};
-      Object.keys(formData).forEach((k) => {
-        let v = resp[k];
-        if (k === 'default_avatar' && !v) {
-          v = 'system';
-        }
-        if (k === 'gravatar_base_url' && !v) {
-          v = '';
-        }
-        formMeta[k] = { ...formData[k], value: v };
-      });
-      setFormData({ ...formData, ...formMeta });
-    });
-  }, []);
-
-  const handleOnChange = (data) => {
-    setFormData(data);
-  };
-
-  return (
-    <>
-      <h3 className="mb-4">{t('title')}</h3>
-      <SchemaForm
-        schema={schema}
-        uiSchema={uiSchema}
-        formData={formData}
-        onSubmit={onSubmit}
-        onChange={handleOnChange}
-      />
-    </>
-  );
-};
-
-export default Index;
diff --git a/ui/src/pages/Admin/Users/index.tsx b/ui/src/pages/Admin/Users/index.tsx
index 2402a839..295cb075 100644
--- a/ui/src/pages/Admin/Users/index.tsx
+++ b/ui/src/pages/Admin/Users/index.tsx
@@ -47,6 +47,7 @@ import { formatCount } from '@/utils';
 
 import DeleteUserModal from './components/DeleteUserModal';
 import Action from './components/Action';
+import SuspenseUserModal from './components/SuspenseUserModal';
 
 const UserFilterKeys: Type.UserFilterBy[] = [
   'normal',
@@ -70,6 +71,10 @@ const Users: FC = () => {
     show: false,
     userId: '',
   });
+  const [suspenseUserModalState, setSuspenseUserModalState] = useState({
+    show: false,
+    userId: '',
+  });
   const [urlSearchParams, setUrlSearchParams] = useSearchParams();
   const curFilter = urlSearchParams.get('filter') || UserFilterKeys[0];
   const curPage = Number(urlSearchParams.get('page') || '1');
@@ -172,6 +177,13 @@ const Users: FC = () => {
     });
   };
 
+  const handleSuspenseUserModalState = (modalData: {
+    show: boolean;
+    userId: string;
+  }) => {
+    setSuspenseUserModalState(modalData);
+  };
+
   const showAddUser =
     !ucAgent?.enabled || (ucAgent?.enabled && adminUcAgent?.allow_create_user);
   const showActionPassword =
@@ -231,17 +243,22 @@ const Users: FC = () => {
           <tr>
             <th>{t('name')}</th>
             <th style={{ width: '12%' }}>{t('reputation')}</th>
-            <th style={{ width: '20%' }} className="min-w-15">
+            <th style={{ width: '15%' }} className="min-w-15">
               {t('email')}
             </th>
-            <th className="text-nowrap" style={{ width: '15%' }}>
+            <th className="text-nowrap" style={{ width: '12%' }}>
               {t('created_at')}
             </th>
             {(curFilter === 'deleted' || curFilter === 'suspended') && (
-              <th className="text-nowrap" style={{ width: '15%' }}>
+              <th className="text-nowrap" style={{ width: '12%' }}>
                 {curFilter === 'deleted' ? t('delete_at') : t('suspend_at')}
               </th>
             )}
+            {curFilter === 'suspended' && (
+              <th className="text-nowrap" style={{ width: '12%' }}>
+                {t('suspend_until')}
+              </th>
+            )}
 
             <th style={{ width: '12%' }}>{t('status')}</th>
             {curFilter !== 'suspended' && curFilter !== 'deleted' && (
@@ -275,9 +292,14 @@ const Users: FC = () => {
                   <FormatTime time={user.created_at} />
                 </td>
                 {curFilter === 'suspended' && (
-                  <td className="text-nowrap">
-                    <FormatTime time={user.suspended_at} />
-                  </td>
+                  <>
+                    <td className="text-nowrap">
+                      <FormatTime time={user.suspended_at} />
+                    </td>
+                    <td className="text-nowrap">
+                      <FormatTime time={user.suspended_at} />
+                    </td>
+                  </>
                 )}
                 {curFilter === 'deleted' && (
                   <td className="text-nowrap">
@@ -306,6 +328,7 @@ const Users: FC = () => {
                     currentUser={currentUser}
                     refreshUsers={refreshUsers}
                     showDeleteModal={changeDeleteUserModalState}
+                    showSuspenseModal={handleSuspenseUserModalState}
                   />
                 ) : null}
               </tr>
@@ -332,6 +355,16 @@ const Users: FC = () => {
         }}
         onDelete={(val) => handleDelete(val)}
       />
+      <SuspenseUserModal
+        show={suspenseUserModalState.show}
+        onClose={() => {
+          handleSuspenseUserModalState({
+            show: false,
+            userId: '',
+          });
+        }}
+        onDelete={(val) => handleDelete(val)}
+      />
     </>
   );
 };
diff --git a/ui/src/pages/Users/Settings/Profile/index.tsx b/ui/src/pages/Users/Settings/Profile/index.tsx
index 9af28ce6..7fb8247c 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, siteInfoStore } from '@/stores';
+import { loggedUserInfoStore, userCenterStore, interfaceStore } 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 { users: usersSetting } = siteInfoStore();
+  const { interface: interfaceSetting } = interfaceStore();
   const [mailHash, setMailHash] = useState('');
   const [count] = useState(0);
   const [profileAgent, setProfileAgent] = useState<UcSettingAgent>();
@@ -309,7 +309,6 @@ const Index: React.FC = () => {
             <Form.Control
               required
               type="text"
-              disabled={!usersSetting.allow_update_display_name}
               value={formData.display_name.value}
               isInvalid={formData.display_name.isInvalid}
               onChange={(e) =>
@@ -332,7 +331,6 @@ const Index: React.FC = () => {
             <Form.Control
               required
               type="text"
-              disabled={!usersSetting.allow_update_username}
               value={formData.username.value}
               isInvalid={formData.username.isInvalid}
               onChange={(e) =>
@@ -356,7 +354,6 @@ const Index: React.FC = () => {
             <div className="mb-3">
               <Form.Select
                 name="avatar.type"
-                disabled={!usersSetting.allow_update_avatar}
                 value={formData.avatar.type}
                 onChange={handleAvatarChange}>
                 <option value="gravatar" key="gravatar">
@@ -387,14 +384,18 @@ const Index: React.FC = () => {
                       <span>{t('avatar.gravatar_text')}</span>
                       <a
                         href={
-                          usersSetting.gravatar_base_url.includes('gravatar.cn')
+                          interfaceSetting.gravatar_base_url.includes(
+                            'gravatar.cn',
+                          )
                             ? 'https://gravatar.cn'
                             : 'https://gravatar.com'
                         }
                         className="ms-1"
                         target="_blank"
                         rel="noreferrer">
-                        {usersSetting.gravatar_base_url.includes('gravatar.cn')
+                        {interfaceSetting.gravatar_base_url.includes(
+                          'gravatar.cn',
+                        )
                           ? 'gravatar.cn'
                           : 'gravatar.com'}
                       </a>
@@ -413,15 +414,11 @@ const Index: React.FC = () => {
                         alt={formData.display_name.value}
                       />
                       <ButtonGroup vertical className="fit-content">
-                        <UploadImg
-                          type="avatar"
-                          disabled={!usersSetting.allow_update_avatar}
-                          uploadCallback={avatarUpload}>
+                        <UploadImg type="avatar" uploadCallback={avatarUpload}>
                           <Icon name="cloud-upload" />
                         </UploadImg>
                         <Button
                           variant="outline-secondary"
-                          disabled={!usersSetting.allow_update_avatar}
                           onClick={removeCustomAvatar}>
                           <Icon name="trash" />
                         </Button>
@@ -463,7 +460,6 @@ const Index: React.FC = () => {
               required
               as="textarea"
               rows={5}
-              disabled={!usersSetting.allow_update_bio}
               value={formData.bio.value}
               isInvalid={formData.bio.isInvalid}
               onChange={(e) =>
@@ -489,7 +485,6 @@ const Index: React.FC = () => {
               required
               type="url"
               placeholder={t('website.placeholder')}
-              disabled={!usersSetting.allow_update_website}
               value={formData.website.value}
               isInvalid={formData.website.isInvalid}
               onChange={(e) =>
@@ -515,7 +510,6 @@ const Index: React.FC = () => {
               required
               type="text"
               placeholder={t('location.placeholder')}
-              disabled={!usersSetting.allow_update_location}
               value={formData.location.value}
               isInvalid={formData.location.isInvalid}
               onChange={(e) =>
diff --git a/ui/src/router/routes.ts b/ui/src/router/routes.ts
index d9d20a44..59907b41 100644
--- a/ui/src/router/routes.ts
+++ b/ui/src/router/routes.ts
@@ -398,10 +398,6 @@ const routes: RouteNode[] = [
             path: 'login',
             page: 'pages/Admin/Login',
           },
-          {
-            path: 'settings-users',
-            page: 'pages/Admin/SettingsUsers',
-          },
           {
             path: 'privileges',
             page: 'pages/Admin/Privileges',
diff --git a/ui/src/services/admin/settings.ts b/ui/src/services/admin/settings.ts
index 7ce57ade..f2b9e598 100644
--- a/ui/src/services/admin/settings.ts
+++ b/ui/src/services/admin/settings.ts
@@ -29,8 +29,6 @@ export interface AdminSettingsUsers {
   allow_update_location: boolean;
   allow_update_username: boolean;
   allow_update_website: boolean;
-  default_avatar: string;
-  gravatar_base_url: string;
 }
 
 interface PrivilegeLevel {
diff --git a/ui/src/stores/interface.ts b/ui/src/stores/interface.ts
index 7b514eb6..cceb425c 100644
--- a/ui/src/stores/interface.ts
+++ b/ui/src/stores/interface.ts
@@ -32,6 +32,7 @@ const interfaceSetting = create<InterfaceType>((set) => ({
     language: DEFAULT_LANG,
     time_zone: '',
     default_avatar: 'system',
+    gravatar_base_url: '',
   },
   update: (params) =>
     set(() => {
diff --git a/ui/src/stores/siteInfo.ts b/ui/src/stores/siteInfo.ts
index 5208b9b4..725546d6 100644
--- a/ui/src/stores/siteInfo.ts
+++ b/ui/src/stores/siteInfo.ts
@@ -39,8 +39,6 @@ const defaultUsersConf: AdminSettingsUsers = {
   allow_update_location: false,
   allow_update_username: false,
   allow_update_website: false,
-  default_avatar: 'system',
-  gravatar_base_url: 'https://www.gravatar.com/avatar/',
 };
 
 const siteInfo = create<SiteInfoType>((set) => ({