Просмотр исходного кода

♻️ refactor(users): modularize UsersTable component into microcomponent architecture

BREAKING CHANGE: Removed standalone user edit routes (/console/user/edit, /console/user/edit/:id)

- Decompose 673-line monolithic UsersTable.js into 8 specialized components
- Extract column definitions to UsersColumnDefs.js with render functions
- Create dedicated UsersActions.jsx for action buttons
- Create UsersFilters.jsx for search and filtering logic
- Create UsersDescription.jsx for description area
- Extract all data management logic to useUsersData.js hook
- Move AddUser.js and EditUser.js to users/modals/ folder as modal components
- Create 4 new confirmation modal components (Promote, Demote, EnableDisable, Delete)
- Implement pure UsersTable.jsx component for table rendering only
- Create main container component users/index.jsx to compose all subcomponents
- Update import paths in pages/User/index.js to use new modular structure
- Remove obsolete EditUser imports and routes from App.js
- Delete original monolithic files: UsersTable.js, AddUser.js, EditUser.js

The new architecture follows the same modular pattern as tokens and redemptions modules:
- Consistent file organization across all table modules
- Better separation of concerns and maintainability
- Enhanced reusability and testability
- Unified modal management approach

All existing functionality preserved with improved code organization.
t0ng7u 7 месяцев назад
Родитель
Сommit
d762da9141

+ 1 - 17
web/src/App.js

@@ -7,7 +7,7 @@ import RegisterForm from './components/auth/RegisterForm.js';
 import LoginForm from './components/auth/LoginForm.js';
 import LoginForm from './components/auth/LoginForm.js';
 import NotFound from './pages/NotFound';
 import NotFound from './pages/NotFound';
 import Setting from './pages/Setting';
 import Setting from './pages/Setting';
-import EditUser from './pages/User/EditUser';
+
 import PasswordResetForm from './components/auth/PasswordResetForm.js';
 import PasswordResetForm from './components/auth/PasswordResetForm.js';
 import PasswordResetConfirm from './components/auth/PasswordResetConfirm.js';
 import PasswordResetConfirm from './components/auth/PasswordResetConfirm.js';
 import Channel from './pages/Channel';
 import Channel from './pages/Channel';
@@ -109,22 +109,6 @@ function App() {
             </PrivateRoute>
             </PrivateRoute>
           }
           }
         />
         />
-        <Route
-          path='/console/user/edit/:id'
-          element={
-            <Suspense fallback={<Loading></Loading>} key={location.pathname}>
-              <EditUser />
-            </Suspense>
-          }
-        />
-        <Route
-          path='/console/user/edit'
-          element={
-            <Suspense fallback={<Loading></Loading>} key={location.pathname}>
-              <EditUser />
-            </Suspense>
-          }
-        />
         <Route
         <Route
           path='/user/reset'
           path='/user/reset'
           element={
           element={

+ 0 - 672
web/src/components/table/UsersTable.js

@@ -1,672 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import { API, showError, showSuccess, renderGroup, renderNumber, renderQuota } from '../../helpers';
-
-import {
-  User,
-  Shield,
-  Crown,
-  HelpCircle,
-  CheckCircle,
-  XCircle,
-  Minus,
-  Coins,
-  Activity,
-  Users,
-  DollarSign,
-  UserPlus,
-} from 'lucide-react';
-import {
-  Button,
-  Dropdown,
-  Empty,
-  Form,
-  Modal,
-  Space,
-  Table,
-  Tag,
-  Tooltip,
-  Typography
-} from '@douyinfe/semi-ui';
-import CardPro from '../common/ui/CardPro';
-import {
-  IllustrationNoResult,
-  IllustrationNoResultDark
-} from '@douyinfe/semi-illustrations';
-import {
-  IconSearch,
-  IconUserAdd,
-  IconMore,
-} from '@douyinfe/semi-icons';
-import { ITEMS_PER_PAGE } from '../../constants';
-import AddUser from '../../pages/User/AddUser';
-import EditUser from '../../pages/User/EditUser';
-import { useTranslation } from 'react-i18next';
-import { useTableCompactMode } from '../../hooks/common/useTableCompactMode';
-
-const { Text } = Typography;
-
-const UsersTable = () => {
-  const { t } = useTranslation();
-  const [compactMode, setCompactMode] = useTableCompactMode('users');
-
-  function renderRole(role) {
-    switch (role) {
-      case 1:
-        return (
-          <Tag color='blue' shape='circle' prefixIcon={<User size={14} />}>
-            {t('普通用户')}
-          </Tag>
-        );
-      case 10:
-        return (
-          <Tag color='yellow' shape='circle' prefixIcon={<Shield size={14} />}>
-            {t('管理员')}
-          </Tag>
-        );
-      case 100:
-        return (
-          <Tag color='orange' shape='circle' prefixIcon={<Crown size={14} />}>
-            {t('超级管理员')}
-          </Tag>
-        );
-      default:
-        return (
-          <Tag color='red' shape='circle' prefixIcon={<HelpCircle size={14} />}>
-            {t('未知身份')}
-          </Tag>
-        );
-    }
-  }
-
-  const renderStatus = (status) => {
-    switch (status) {
-      case 1:
-        return <Tag color='green' shape='circle' prefixIcon={<CheckCircle size={14} />}>{t('已激活')}</Tag>;
-      case 2:
-        return (
-          <Tag color='red' shape='circle' prefixIcon={<XCircle size={14} />}>
-            {t('已封禁')}
-          </Tag>
-        );
-      default:
-        return (
-          <Tag color='grey' shape='circle' prefixIcon={<HelpCircle size={14} />}>
-            {t('未知状态')}
-          </Tag>
-        );
-    }
-  };
-
-  const columns = [
-    {
-      title: 'ID',
-      dataIndex: 'id',
-    },
-    {
-      title: t('用户名'),
-      dataIndex: 'username',
-      render: (text, record) => {
-        const remark = record.remark;
-        if (!remark) {
-          return <span>{text}</span>;
-        }
-        const maxLen = 10;
-        const displayRemark = remark.length > maxLen ? remark.slice(0, maxLen) + '…' : remark;
-        return (
-          <Space spacing={2}>
-            <span>{text}</span>
-            <Tooltip content={remark} position="top" showArrow>
-              <Tag color='white' shape='circle' className="!text-xs">
-                <div className="flex items-center gap-1">
-                  <div className="w-2 h-2 flex-shrink-0 rounded-full" style={{ backgroundColor: '#10b981' }} />
-                  {displayRemark}
-                </div>
-              </Tag>
-            </Tooltip>
-          </Space>
-        );
-      },
-    },
-    {
-      title: t('分组'),
-      dataIndex: 'group',
-      render: (text, record, index) => {
-        return <div>{renderGroup(text)}</div>;
-      },
-    },
-    {
-      title: t('统计信息'),
-      dataIndex: 'info',
-      render: (text, record, index) => {
-        return (
-          <div>
-            <Space spacing={1}>
-              <Tag color='white' shape='circle' className="!text-xs" prefixIcon={<Coins size={14} />}>
-                {t('剩余')}: {renderQuota(record.quota)}
-              </Tag>
-              <Tag color='white' shape='circle' className="!text-xs" prefixIcon={<Coins size={14} />}>
-                {t('已用')}: {renderQuota(record.used_quota)}
-              </Tag>
-              <Tag color='white' shape='circle' className="!text-xs" prefixIcon={<Activity size={14} />}>
-                {t('调用')}: {renderNumber(record.request_count)}
-              </Tag>
-            </Space>
-          </div>
-        );
-      },
-    },
-    {
-      title: t('邀请信息'),
-      dataIndex: 'invite',
-      render: (text, record, index) => {
-        return (
-          <div>
-            <Space spacing={1}>
-              <Tag color='white' shape='circle' className="!text-xs" prefixIcon={<Users size={14} />}>
-                {t('邀请')}: {renderNumber(record.aff_count)}
-              </Tag>
-              <Tag color='white' shape='circle' className="!text-xs" prefixIcon={<DollarSign size={14} />}>
-                {t('收益')}: {renderQuota(record.aff_history_quota)}
-              </Tag>
-              <Tag color='white' shape='circle' className="!text-xs" prefixIcon={<UserPlus size={14} />}>
-                {record.inviter_id === 0 ? t('无邀请人') : `邀请人: ${record.inviter_id}`}
-              </Tag>
-            </Space>
-          </div>
-        );
-      },
-    },
-    {
-      title: t('角色'),
-      dataIndex: 'role',
-      render: (text, record, index) => {
-        return <div>{renderRole(text)}</div>;
-      },
-    },
-    {
-      title: t('状态'),
-      dataIndex: 'status',
-      render: (text, record, index) => {
-        return (
-          <div>
-            {record.DeletedAt !== null ? (
-              <Tag color='red' shape='circle' prefixIcon={<Minus size={14} />}>{t('已注销')}</Tag>
-            ) : (
-              renderStatus(text)
-            )}
-          </div>
-        );
-      },
-    },
-    {
-      title: '',
-      dataIndex: 'operate',
-      fixed: 'right',
-      render: (text, record, index) => {
-        if (record.DeletedAt !== null) {
-          return <></>;
-        }
-
-        // 创建更多操作的下拉菜单项
-        const moreMenuItems = [
-          {
-            node: 'item',
-            name: t('提升'),
-            type: 'warning',
-            onClick: () => {
-              Modal.confirm({
-                title: t('确定要提升此用户吗?'),
-                content: t('此操作将提升用户的权限级别'),
-                onOk: () => {
-                  manageUser(record.id, 'promote', record);
-                },
-              });
-            },
-          },
-          {
-            node: 'item',
-            name: t('降级'),
-            type: 'secondary',
-            onClick: () => {
-              Modal.confirm({
-                title: t('确定要降级此用户吗?'),
-                content: t('此操作将降低用户的权限级别'),
-                onOk: () => {
-                  manageUser(record.id, 'demote', record);
-                },
-              });
-            },
-          },
-          {
-            node: 'item',
-            name: t('注销'),
-            type: 'danger',
-            onClick: () => {
-              Modal.confirm({
-                title: t('确定是否要注销此用户?'),
-                content: t('相当于删除用户,此修改将不可逆'),
-                onOk: () => {
-                  (async () => {
-                    await manageUser(record.id, 'delete', record);
-                    await refresh();
-                    setTimeout(() => {
-                      if (users.length === 0 && activePage > 1) {
-                        refresh(activePage - 1);
-                      }
-                    }, 100);
-                  })();
-                },
-              });
-            },
-          }
-        ];
-
-        // 动态添加启用/禁用按钮
-        if (record.status === 1) {
-          moreMenuItems.splice(-1, 0, {
-            node: 'item',
-            name: t('禁用'),
-            type: 'warning',
-            onClick: () => {
-              manageUser(record.id, 'disable', record);
-            },
-          });
-        } else {
-          moreMenuItems.splice(-1, 0, {
-            node: 'item',
-            name: t('启用'),
-            type: 'secondary',
-            onClick: () => {
-              manageUser(record.id, 'enable', record);
-            },
-            disabled: record.status === 3,
-          });
-        }
-
-        return (
-          <Space>
-            <Button
-              type='tertiary'
-              size="small"
-              onClick={() => {
-                setEditingUser(record);
-                setShowEditUser(true);
-              }}
-            >
-              {t('编辑')}
-            </Button>
-            <Dropdown
-              trigger='click'
-              position='bottomRight'
-              menu={moreMenuItems}
-            >
-              <Button
-                type='tertiary'
-                size="small"
-                icon={<IconMore />}
-              />
-            </Dropdown>
-          </Space>
-        );
-      },
-    },
-  ];
-
-  const [users, setUsers] = useState([]);
-  const [loading, setLoading] = useState(true);
-  const [activePage, setActivePage] = useState(1);
-  const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
-  const [searching, setSearching] = useState(false);
-  const [groupOptions, setGroupOptions] = useState([]);
-  const [userCount, setUserCount] = useState(ITEMS_PER_PAGE);
-  const [showAddUser, setShowAddUser] = useState(false);
-  const [showEditUser, setShowEditUser] = useState(false);
-  const [editingUser, setEditingUser] = useState({
-    id: undefined,
-  });
-
-  // Form 初始值
-  const formInitValues = {
-    searchKeyword: '',
-    searchGroup: '',
-  };
-
-  // Form API 引用
-  const [formApi, setFormApi] = useState(null);
-
-  // 获取表单值的辅助函数
-  const getFormValues = () => {
-    const formValues = formApi ? formApi.getValues() : {};
-    return {
-      searchKeyword: formValues.searchKeyword || '',
-      searchGroup: formValues.searchGroup || '',
-    };
-  };
-
-  const removeRecord = (key) => {
-    let newDataSource = [...users];
-    if (key != null) {
-      let idx = newDataSource.findIndex((data) => data.id === key);
-
-      if (idx > -1) {
-        // update deletedAt
-        newDataSource[idx].DeletedAt = new Date();
-        setUsers(newDataSource);
-      }
-    }
-  };
-
-  const setUserFormat = (users) => {
-    for (let i = 0; i < users.length; i++) {
-      users[i].key = users[i].id;
-    }
-    setUsers(users);
-  };
-
-  const loadUsers = async (startIdx, pageSize) => {
-    const res = await API.get(`/api/user/?p=${startIdx}&page_size=${pageSize}`);
-    const { success, message, data } = res.data;
-    if (success) {
-      const newPageData = data.items;
-      setActivePage(data.page);
-      setUserCount(data.total);
-      setUserFormat(newPageData);
-    } else {
-      showError(message);
-    }
-    setLoading(false);
-  };
-
-  useEffect(() => {
-    loadUsers(0, pageSize)
-      .then()
-      .catch((reason) => {
-        showError(reason);
-      });
-    fetchGroups().then();
-  }, []);
-
-  const manageUser = async (userId, action, record) => {
-    const res = await API.post('/api/user/manage', {
-      id: userId,
-      action,
-    });
-    const { success, message } = res.data;
-    if (success) {
-      showSuccess('操作成功完成!');
-      let user = res.data.data;
-      let newUsers = [...users];
-      if (action === 'delete') {
-      } else {
-        record.status = user.status;
-        record.role = user.role;
-      }
-      setUsers(newUsers);
-    } else {
-      showError(message);
-    }
-  };
-
-  const searchUsers = async (
-    startIdx,
-    pageSize,
-    searchKeyword = null,
-    searchGroup = null,
-  ) => {
-    // 如果没有传递参数,从表单获取值
-    if (searchKeyword === null || searchGroup === null) {
-      const formValues = getFormValues();
-      searchKeyword = formValues.searchKeyword;
-      searchGroup = formValues.searchGroup;
-    }
-
-    if (searchKeyword === '' && searchGroup === '') {
-      // if keyword is blank, load files instead.
-      await loadUsers(startIdx, pageSize);
-      return;
-    }
-    setSearching(true);
-    const res = await API.get(
-      `/api/user/search?keyword=${searchKeyword}&group=${searchGroup}&p=${startIdx}&page_size=${pageSize}`,
-    );
-    const { success, message, data } = res.data;
-    if (success) {
-      const newPageData = data.items;
-      setActivePage(data.page);
-      setUserCount(data.total);
-      setUserFormat(newPageData);
-    } else {
-      showError(message);
-    }
-    setSearching(false);
-  };
-
-  const handlePageChange = (page) => {
-    setActivePage(page);
-    const { searchKeyword, searchGroup } = getFormValues();
-    if (searchKeyword === '' && searchGroup === '') {
-      loadUsers(page, pageSize).then();
-    } else {
-      searchUsers(page, pageSize, searchKeyword, searchGroup).then();
-    }
-  };
-
-  const closeAddUser = () => {
-    setShowAddUser(false);
-  };
-
-  const closeEditUser = () => {
-    setShowEditUser(false);
-    setEditingUser({
-      id: undefined,
-    });
-  };
-
-  const refresh = async (page = activePage) => {
-    const { searchKeyword, searchGroup } = getFormValues();
-    if (searchKeyword === '' && searchGroup === '') {
-      await loadUsers(page, pageSize);
-    } else {
-      await searchUsers(page, pageSize, searchKeyword, searchGroup);
-    }
-  };
-
-  const fetchGroups = async () => {
-    try {
-      let res = await API.get(`/api/group/`);
-      // add 'all' option
-      // res.data.data.unshift('all');
-      if (res === undefined) {
-        return;
-      }
-      setGroupOptions(
-        res.data.data.map((group) => ({
-          label: group,
-          value: group,
-        })),
-      );
-    } catch (error) {
-      showError(error.message);
-    }
-  };
-
-  const handlePageSizeChange = async (size) => {
-    localStorage.setItem('page-size', size + '');
-    setPageSize(size);
-    setActivePage(1);
-    loadUsers(activePage, size)
-      .then()
-      .catch((reason) => {
-        showError(reason);
-      });
-  };
-
-  const handleRow = (record, index) => {
-    if (record.DeletedAt !== null || record.status !== 1) {
-      return {
-        style: {
-          background: 'var(--semi-color-disabled-border)',
-        },
-      };
-    } else {
-      return {};
-    }
-  };
-
-
-
-  return (
-    <>
-      <AddUser
-        refresh={refresh}
-        visible={showAddUser}
-        handleClose={closeAddUser}
-      ></AddUser>
-      <EditUser
-        refresh={refresh}
-        visible={showEditUser}
-        handleClose={closeEditUser}
-        editingUser={editingUser}
-      ></EditUser>
-
-      <CardPro
-        type="type1"
-        descriptionArea={
-          <div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-2 w-full">
-            <div className="flex items-center text-blue-500">
-              <IconUserAdd className="mr-2" />
-              <Text>{t('用户管理页面,可以查看和管理所有注册用户的信息、权限和状态。')}</Text>
-            </div>
-            <Button
-              type='tertiary'
-              className="w-full md:w-auto"
-              onClick={() => setCompactMode(!compactMode)}
-              size="small"
-            >
-              {compactMode ? t('自适应列表') : t('紧凑列表')}
-            </Button>
-          </div>
-        }
-        actionsArea={
-          <div className="flex flex-col md:flex-row justify-between items-center gap-4 w-full">
-            <div className="flex gap-2 w-full md:w-auto order-2 md:order-1">
-              <Button
-                className="w-full md:w-auto"
-                onClick={() => {
-                  setShowAddUser(true);
-                }}
-                size="small"
-              >
-                {t('添加用户')}
-              </Button>
-            </div>
-
-            <Form
-              initValues={formInitValues}
-              getFormApi={(api) => setFormApi(api)}
-              onSubmit={() => {
-                setActivePage(1);
-                searchUsers(1, pageSize);
-              }}
-              allowEmpty={true}
-              autoComplete="off"
-              layout="horizontal"
-              trigger="change"
-              stopValidateWithError={false}
-              className="w-full md:w-auto order-1 md:order-2"
-            >
-              <div className="flex flex-col md:flex-row items-center gap-4 w-full md:w-auto">
-                <div className="relative w-full md:w-64">
-                  <Form.Input
-                    field="searchKeyword"
-                    prefix={<IconSearch />}
-                    placeholder={t('支持搜索用户的 ID、用户名、显示名称和邮箱地址')}
-                    showClear
-                    pure
-                    size="small"
-                  />
-                </div>
-                <div className="w-full md:w-48">
-                  <Form.Select
-                    field="searchGroup"
-                    placeholder={t('选择分组')}
-                    optionList={groupOptions}
-                    onChange={(value) => {
-                      // 分组变化时自动搜索
-                      setTimeout(() => {
-                        setActivePage(1);
-                        searchUsers(1, pageSize);
-                      }, 100);
-                    }}
-                    className="w-full"
-                    showClear
-                    pure
-                    size="small"
-                  />
-                </div>
-                <div className="flex gap-2 w-full md:w-auto">
-                  <Button
-                    type="tertiary"
-                    htmlType="submit"
-                    loading={loading || searching}
-                    className="flex-1 md:flex-initial md:w-auto"
-                    size="small"
-                  >
-                    {t('查询')}
-                  </Button>
-                  <Button
-                    type='tertiary'
-                    onClick={() => {
-                      if (formApi) {
-                        formApi.reset();
-                        setTimeout(() => {
-                          setActivePage(1);
-                          loadUsers(1, pageSize);
-                        }, 100);
-                      }
-                    }}
-                    className="flex-1 md:flex-initial md:w-auto"
-                    size="small"
-                  >
-                    {t('重置')}
-                  </Button>
-                </div>
-              </div>
-            </Form>
-          </div>
-        }
-      >
-        <Table
-          columns={compactMode ? columns.map(({ fixed, ...rest }) => rest) : columns}
-          dataSource={users}
-          scroll={compactMode ? undefined : { x: 'max-content' }}
-          pagination={{
-            currentPage: activePage,
-            pageSize: pageSize,
-            total: userCount,
-            pageSizeOpts: [10, 20, 50, 100],
-            showSizeChanger: true,
-            onPageSizeChange: (size) => {
-              handlePageSizeChange(size);
-            },
-            onPageChange: handlePageChange,
-          }}
-          loading={loading}
-          onRow={handleRow}
-          empty={
-            <Empty
-              image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
-              darkModeImage={<IllustrationNoResultDark style={{ width: 150, height: 150 }} />}
-              description={t('搜索无结果')}
-              style={{ padding: 30 }}
-            />
-          }
-          className="overflow-hidden"
-          size="middle"
-        />
-      </CardPro>
-    </>
-  );
-};
-
-export default UsersTable;

+ 27 - 0
web/src/components/table/users/UsersActions.jsx

@@ -0,0 +1,27 @@
+import React from 'react';
+import { Button } from '@douyinfe/semi-ui';
+
+const UsersActions = ({
+  setShowAddUser,
+  t
+}) => {
+
+  // Add new user
+  const handleAddUser = () => {
+    setShowAddUser(true);
+  };
+
+  return (
+    <div className="flex gap-2 w-full md:w-auto order-2 md:order-1">
+      <Button
+        className="w-full md:w-auto"
+        onClick={handleAddUser}
+        size="small"
+      >
+        {t('添加用户')}
+      </Button>
+    </div>
+  );
+};
+
+export default UsersActions; 

+ 310 - 0
web/src/components/table/users/UsersColumnDefs.js

@@ -0,0 +1,310 @@
+import React from 'react';
+import {
+  Button,
+  Dropdown,
+  Space,
+  Tag,
+  Tooltip,
+  Typography
+} from '@douyinfe/semi-ui';
+import {
+  User,
+  Shield,
+  Crown,
+  HelpCircle,
+  CheckCircle,
+  XCircle,
+  Minus,
+  Coins,
+  Activity,
+  Users,
+  DollarSign,
+  UserPlus,
+} from 'lucide-react';
+import { IconMore } from '@douyinfe/semi-icons';
+import { renderGroup, renderNumber, renderQuota } from '../../../helpers';
+
+const { Text } = Typography;
+
+/**
+ * Render user role
+ */
+const renderRole = (role, t) => {
+  switch (role) {
+    case 1:
+      return (
+        <Tag color='blue' shape='circle' prefixIcon={<User size={14} />}>
+          {t('普通用户')}
+        </Tag>
+      );
+    case 10:
+      return (
+        <Tag color='yellow' shape='circle' prefixIcon={<Shield size={14} />}>
+          {t('管理员')}
+        </Tag>
+      );
+    case 100:
+      return (
+        <Tag color='orange' shape='circle' prefixIcon={<Crown size={14} />}>
+          {t('超级管理员')}
+        </Tag>
+      );
+    default:
+      return (
+        <Tag color='red' shape='circle' prefixIcon={<HelpCircle size={14} />}>
+          {t('未知身份')}
+        </Tag>
+      );
+  }
+};
+
+/**
+ * Render user status
+ */
+const renderStatus = (status, t) => {
+  switch (status) {
+    case 1:
+      return <Tag color='green' shape='circle' prefixIcon={<CheckCircle size={14} />}>{t('已激活')}</Tag>;
+    case 2:
+      return (
+        <Tag color='red' shape='circle' prefixIcon={<XCircle size={14} />}>
+          {t('已封禁')}
+        </Tag>
+      );
+    default:
+      return (
+        <Tag color='grey' shape='circle' prefixIcon={<HelpCircle size={14} />}>
+          {t('未知状态')}
+        </Tag>
+      );
+  }
+};
+
+/**
+ * Render username with remark
+ */
+const renderUsername = (text, record) => {
+  const remark = record.remark;
+  if (!remark) {
+    return <span>{text}</span>;
+  }
+  const maxLen = 10;
+  const displayRemark = remark.length > maxLen ? remark.slice(0, maxLen) + '…' : remark;
+  return (
+    <Space spacing={2}>
+      <span>{text}</span>
+      <Tooltip content={remark} position="top" showArrow>
+        <Tag color='white' shape='circle' className="!text-xs">
+          <div className="flex items-center gap-1">
+            <div className="w-2 h-2 flex-shrink-0 rounded-full" style={{ backgroundColor: '#10b981' }} />
+            {displayRemark}
+          </div>
+        </Tag>
+      </Tooltip>
+    </Space>
+  );
+};
+
+/**
+ * Render user statistics
+ */
+const renderStatistics = (text, record, t) => {
+  return (
+    <div>
+      <Space spacing={1}>
+        <Tag color='white' shape='circle' className="!text-xs" prefixIcon={<Coins size={14} />}>
+          {t('剩余')}: {renderQuota(record.quota)}
+        </Tag>
+        <Tag color='white' shape='circle' className="!text-xs" prefixIcon={<Coins size={14} />}>
+          {t('已用')}: {renderQuota(record.used_quota)}
+        </Tag>
+        <Tag color='white' shape='circle' className="!text-xs" prefixIcon={<Activity size={14} />}>
+          {t('调用')}: {renderNumber(record.request_count)}
+        </Tag>
+      </Space>
+    </div>
+  );
+};
+
+/**
+ * Render invite information
+ */
+const renderInviteInfo = (text, record, t) => {
+  return (
+    <div>
+      <Space spacing={1}>
+        <Tag color='white' shape='circle' className="!text-xs" prefixIcon={<Users size={14} />}>
+          {t('邀请')}: {renderNumber(record.aff_count)}
+        </Tag>
+        <Tag color='white' shape='circle' className="!text-xs" prefixIcon={<DollarSign size={14} />}>
+          {t('收益')}: {renderQuota(record.aff_history_quota)}
+        </Tag>
+        <Tag color='white' shape='circle' className="!text-xs" prefixIcon={<UserPlus size={14} />}>
+          {record.inviter_id === 0 ? t('无邀请人') : `邀请人: ${record.inviter_id}`}
+        </Tag>
+      </Space>
+    </div>
+  );
+};
+
+/**
+ * Render overall status including deleted status
+ */
+const renderOverallStatus = (status, record, t) => {
+  if (record.DeletedAt !== null) {
+    return <Tag color='red' shape='circle' prefixIcon={<Minus size={14} />}>{t('已注销')}</Tag>;
+  } else {
+    return renderStatus(status, t);
+  }
+};
+
+/**
+ * Render operations column
+ */
+const renderOperations = (text, record, {
+  setEditingUser,
+  setShowEditUser,
+  showPromoteModal,
+  showDemoteModal,
+  showEnableDisableModal,
+  showDeleteModal,
+  t
+}) => {
+  if (record.DeletedAt !== null) {
+    return <></>;
+  }
+
+  // Create more operations dropdown menu items
+  const moreMenuItems = [
+    {
+      node: 'item',
+      name: t('提升'),
+      type: 'warning',
+      onClick: () => showPromoteModal(record),
+    },
+    {
+      node: 'item',
+      name: t('降级'),
+      type: 'secondary',
+      onClick: () => showDemoteModal(record),
+    },
+    {
+      node: 'item',
+      name: t('注销'),
+      type: 'danger',
+      onClick: () => showDeleteModal(record),
+    }
+  ];
+
+  // Add enable/disable button dynamically
+  if (record.status === 1) {
+    moreMenuItems.splice(-1, 0, {
+      node: 'item',
+      name: t('禁用'),
+      type: 'warning',
+      onClick: () => showEnableDisableModal(record, 'disable'),
+    });
+  } else {
+    moreMenuItems.splice(-1, 0, {
+      node: 'item',
+      name: t('启用'),
+      type: 'secondary',
+      onClick: () => showEnableDisableModal(record, 'enable'),
+      disabled: record.status === 3,
+    });
+  }
+
+  return (
+    <Space>
+      <Button
+        type='tertiary'
+        size="small"
+        onClick={() => {
+          setEditingUser(record);
+          setShowEditUser(true);
+        }}
+      >
+        {t('编辑')}
+      </Button>
+      <Dropdown
+        trigger='click'
+        position='bottomRight'
+        menu={moreMenuItems}
+      >
+        <Button
+          type='tertiary'
+          size="small"
+          icon={<IconMore />}
+        />
+      </Dropdown>
+    </Space>
+  );
+};
+
+/**
+ * Get users table column definitions
+ */
+export const getUsersColumns = ({
+  t,
+  setEditingUser,
+  setShowEditUser,
+  showPromoteModal,
+  showDemoteModal,
+  showEnableDisableModal,
+  showDeleteModal
+}) => {
+  return [
+    {
+      title: 'ID',
+      dataIndex: 'id',
+    },
+    {
+      title: t('用户名'),
+      dataIndex: 'username',
+      render: (text, record) => renderUsername(text, record),
+    },
+    {
+      title: t('分组'),
+      dataIndex: 'group',
+      render: (text, record, index) => {
+        return <div>{renderGroup(text)}</div>;
+      },
+    },
+    {
+      title: t('统计信息'),
+      dataIndex: 'info',
+      render: (text, record, index) => renderStatistics(text, record, t),
+    },
+    {
+      title: t('邀请信息'),
+      dataIndex: 'invite',
+      render: (text, record, index) => renderInviteInfo(text, record, t),
+    },
+    {
+      title: t('角色'),
+      dataIndex: 'role',
+      render: (text, record, index) => {
+        return <div>{renderRole(text, t)}</div>;
+      },
+    },
+    {
+      title: t('状态'),
+      dataIndex: 'status',
+      render: (text, record, index) => renderOverallStatus(text, record, t),
+    },
+    {
+      title: '',
+      dataIndex: 'operate',
+      fixed: 'right',
+      render: (text, record, index) => renderOperations(text, record, {
+        setEditingUser,
+        setShowEditUser,
+        showPromoteModal,
+        showDemoteModal,
+        showEnableDisableModal,
+        showDeleteModal,
+        t
+      }),
+    },
+  ];
+}; 

+ 26 - 0
web/src/components/table/users/UsersDescription.jsx

@@ -0,0 +1,26 @@
+import React from 'react';
+import { Button, Typography } from '@douyinfe/semi-ui';
+import { IconUserAdd } from '@douyinfe/semi-icons';
+
+const { Text } = Typography;
+
+const UsersDescription = ({ compactMode, setCompactMode, t }) => {
+  return (
+    <div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-2 w-full">
+      <div className="flex items-center text-blue-500">
+        <IconUserAdd className="mr-2" />
+        <Text>{t('用户管理页面,可以查看和管理所有注册用户的信息、权限和状态。')}</Text>
+      </div>
+      <Button
+        type='tertiary'
+        className="w-full md:w-auto"
+        onClick={() => setCompactMode(!compactMode)}
+        size="small"
+      >
+        {compactMode ? t('自适应列表') : t('紧凑列表')}
+      </Button>
+    </div>
+  );
+};
+
+export default UsersDescription; 

+ 95 - 0
web/src/components/table/users/UsersFilters.jsx

@@ -0,0 +1,95 @@
+import React from 'react';
+import { Form, Button } from '@douyinfe/semi-ui';
+import { IconSearch } from '@douyinfe/semi-icons';
+
+const UsersFilters = ({
+  formInitValues,
+  setFormApi,
+  searchUsers,
+  loadUsers,
+  activePage,
+  pageSize,
+  groupOptions,
+  loading,
+  searching,
+  t
+}) => {
+
+  // Handle form reset and immediate search
+  const handleReset = (formApi) => {
+    if (formApi) {
+      formApi.reset();
+      // Reset and search immediately
+      setTimeout(() => {
+        loadUsers(1, pageSize);
+      }, 100);
+    }
+  };
+
+  return (
+    <Form
+      initValues={formInitValues}
+      getFormApi={(api) => setFormApi(api)}
+      onSubmit={() => {
+        searchUsers(1, pageSize);
+      }}
+      allowEmpty={true}
+      autoComplete="off"
+      layout="horizontal"
+      trigger="change"
+      stopValidateWithError={false}
+      className="w-full md:w-auto order-1 md:order-2"
+    >
+      <div className="flex flex-col md:flex-row items-center gap-2 w-full md:w-auto">
+        <div className="relative w-full md:w-64">
+          <Form.Input
+            field="searchKeyword"
+            prefix={<IconSearch />}
+            placeholder={t('支持搜索用户的 ID、用户名、显示名称和邮箱地址')}
+            showClear
+            pure
+            size="small"
+          />
+        </div>
+        <div className="w-full md:w-48">
+          <Form.Select
+            field="searchGroup"
+            placeholder={t('选择分组')}
+            optionList={groupOptions}
+            onChange={(value) => {
+              // Group change triggers automatic search
+              setTimeout(() => {
+                searchUsers(1, pageSize);
+              }, 100);
+            }}
+            className="w-full"
+            showClear
+            pure
+            size="small"
+          />
+        </div>
+        <div className="flex gap-2 w-full md:w-auto">
+          <Button
+            type="tertiary"
+            htmlType="submit"
+            loading={loading || searching}
+            className="flex-1 md:flex-initial md:w-auto"
+            size="small"
+          >
+            {t('查询')}
+          </Button>
+          <Button
+            type='tertiary'
+            onClick={(_, formApi) => handleReset(formApi)}
+            className="flex-1 md:flex-initial md:w-auto"
+            size="small"
+          >
+            {t('重置')}
+          </Button>
+        </div>
+      </div>
+    </Form>
+  );
+};
+
+export default UsersFilters; 

+ 174 - 0
web/src/components/table/users/UsersTable.jsx

@@ -0,0 +1,174 @@
+import React, { useMemo, useState } from 'react';
+import { Table, Empty } from '@douyinfe/semi-ui';
+import {
+  IllustrationNoResult,
+  IllustrationNoResultDark
+} from '@douyinfe/semi-illustrations';
+import { getUsersColumns } from './UsersColumnDefs';
+import PromoteUserModal from './modals/PromoteUserModal';
+import DemoteUserModal from './modals/DemoteUserModal';
+import EnableDisableUserModal from './modals/EnableDisableUserModal';
+import DeleteUserModal from './modals/DeleteUserModal';
+
+const UsersTable = (usersData) => {
+  const {
+    users,
+    loading,
+    activePage,
+    pageSize,
+    userCount,
+    compactMode,
+    handlePageChange,
+    handlePageSizeChange,
+    handleRow,
+    setEditingUser,
+    setShowEditUser,
+    manageUser,
+    refresh,
+    t,
+  } = usersData;
+
+  // Modal states
+  const [showPromoteModal, setShowPromoteModal] = useState(false);
+  const [showDemoteModal, setShowDemoteModal] = useState(false);
+  const [showEnableDisableModal, setShowEnableDisableModal] = useState(false);
+  const [showDeleteModal, setShowDeleteModal] = useState(false);
+  const [modalUser, setModalUser] = useState(null);
+  const [enableDisableAction, setEnableDisableAction] = useState('');
+
+  // Modal handlers
+  const showPromoteUserModal = (user) => {
+    setModalUser(user);
+    setShowPromoteModal(true);
+  };
+
+  const showDemoteUserModal = (user) => {
+    setModalUser(user);
+    setShowDemoteModal(true);
+  };
+
+  const showEnableDisableUserModal = (user, action) => {
+    setModalUser(user);
+    setEnableDisableAction(action);
+    setShowEnableDisableModal(true);
+  };
+
+  const showDeleteUserModal = (user) => {
+    setModalUser(user);
+    setShowDeleteModal(true);
+  };
+
+  // Modal confirm handlers
+  const handlePromoteConfirm = () => {
+    manageUser(modalUser.id, 'promote', modalUser);
+    setShowPromoteModal(false);
+  };
+
+  const handleDemoteConfirm = () => {
+    manageUser(modalUser.id, 'demote', modalUser);
+    setShowDemoteModal(false);
+  };
+
+  const handleEnableDisableConfirm = () => {
+    manageUser(modalUser.id, enableDisableAction, modalUser);
+    setShowEnableDisableModal(false);
+  };
+
+  // Get all columns
+  const columns = useMemo(() => {
+    return getUsersColumns({
+      t,
+      setEditingUser,
+      setShowEditUser,
+      showPromoteModal: showPromoteUserModal,
+      showDemoteModal: showDemoteUserModal,
+      showEnableDisableModal: showEnableDisableUserModal,
+      showDeleteModal: showDeleteUserModal
+    });
+  }, [
+    t,
+    setEditingUser,
+    setShowEditUser,
+  ]);
+
+  // Handle compact mode by removing fixed positioning
+  const tableColumns = useMemo(() => {
+    return compactMode ? columns.map(col => {
+      if (col.dataIndex === 'operate') {
+        const { fixed, ...rest } = col;
+        return rest;
+      }
+      return col;
+    }) : columns;
+  }, [compactMode, columns]);
+
+  return (
+    <>
+      <Table
+        columns={tableColumns}
+        dataSource={users}
+        scroll={compactMode ? undefined : { x: 'max-content' }}
+        pagination={{
+          currentPage: activePage,
+          pageSize: pageSize,
+          total: userCount,
+          pageSizeOpts: [10, 20, 50, 100],
+          showSizeChanger: true,
+          onPageSizeChange: handlePageSizeChange,
+          onPageChange: handlePageChange,
+        }}
+        loading={loading}
+        onRow={handleRow}
+        empty={
+          <Empty
+            image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
+            darkModeImage={<IllustrationNoResultDark style={{ width: 150, height: 150 }} />}
+            description={t('搜索无结果')}
+            style={{ padding: 30 }}
+          />
+        }
+        className="overflow-hidden"
+        size="middle"
+      />
+
+      {/* Modal components */}
+      <PromoteUserModal
+        visible={showPromoteModal}
+        onCancel={() => setShowPromoteModal(false)}
+        onConfirm={handlePromoteConfirm}
+        user={modalUser}
+        t={t}
+      />
+
+      <DemoteUserModal
+        visible={showDemoteModal}
+        onCancel={() => setShowDemoteModal(false)}
+        onConfirm={handleDemoteConfirm}
+        user={modalUser}
+        t={t}
+      />
+
+      <EnableDisableUserModal
+        visible={showEnableDisableModal}
+        onCancel={() => setShowEnableDisableModal(false)}
+        onConfirm={handleEnableDisableConfirm}
+        user={modalUser}
+        action={enableDisableAction}
+        t={t}
+      />
+
+      <DeleteUserModal
+        visible={showDeleteModal}
+        onCancel={() => setShowDeleteModal(false)}
+        user={modalUser}
+        users={users}
+        activePage={activePage}
+        refresh={refresh}
+        manageUser={manageUser}
+        t={t}
+      />
+    </>
+  );
+};
+
+export default UsersTable; 

+ 95 - 0
web/src/components/table/users/index.jsx

@@ -0,0 +1,95 @@
+import React from 'react';
+import CardPro from '../../common/ui/CardPro';
+import UsersTable from './UsersTable.jsx';
+import UsersActions from './UsersActions.jsx';
+import UsersFilters from './UsersFilters.jsx';
+import UsersDescription from './UsersDescription.jsx';
+import AddUserModal from './modals/AddUserModal.jsx';
+import EditUserModal from './modals/EditUserModal.jsx';
+import { useUsersData } from '../../../hooks/users/useUsersData';
+
+const UsersPage = () => {
+  const usersData = useUsersData();
+
+  const {
+    // Modal state
+    showAddUser,
+    showEditUser,
+    editingUser,
+    setShowAddUser,
+    closeAddUser,
+    closeEditUser,
+    refresh,
+
+    // Form state
+    formInitValues,
+    setFormApi,
+    searchUsers,
+    loadUsers,
+    activePage,
+    pageSize,
+    groupOptions,
+    loading,
+    searching,
+
+    // Description state
+    compactMode,
+    setCompactMode,
+
+    // Translation
+    t,
+  } = usersData;
+
+  return (
+    <>
+      <AddUserModal
+        refresh={refresh}
+        visible={showAddUser}
+        handleClose={closeAddUser}
+      />
+      
+      <EditUserModal
+        refresh={refresh}
+        visible={showEditUser}
+        handleClose={closeEditUser}
+        editingUser={editingUser}
+      />
+
+      <CardPro
+        type="type1"
+        descriptionArea={
+          <UsersDescription
+            compactMode={compactMode}
+            setCompactMode={setCompactMode}
+            t={t}
+          />
+        }
+        actionsArea={
+          <div className="flex flex-col md:flex-row justify-between items-center gap-2 w-full">
+            <UsersActions
+              setShowAddUser={setShowAddUser}
+              t={t}
+            />
+
+            <UsersFilters
+              formInitValues={formInitValues}
+              setFormApi={setFormApi}
+              searchUsers={searchUsers}
+              loadUsers={loadUsers}
+              activePage={activePage}
+              pageSize={pageSize}
+              groupOptions={groupOptions}
+              loading={loading}
+              searching={searching}
+              t={t}
+            />
+          </div>
+        }
+      >
+        <UsersTable {...usersData} />
+      </CardPro>
+    </>
+  );
+};
+
+export default UsersPage; 

+ 4 - 4
web/src/pages/User/AddUser.js → web/src/components/table/users/modals/AddUserModal.jsx

@@ -1,6 +1,6 @@
 import React, { useState, useRef } from 'react';
 import React, { useState, useRef } from 'react';
-import { API, showError, showSuccess } from '../../helpers';
-import { useIsMobile } from '../../hooks/common/useIsMobile.js';
+import { API, showError, showSuccess } from '../../../../helpers';
+import { useIsMobile } from '../../../../hooks/common/useIsMobile.js';
 import {
 import {
   Button,
   Button,
   SideSheet,
   SideSheet,
@@ -23,7 +23,7 @@ import { useTranslation } from 'react-i18next';
 
 
 const { Text, Title } = Typography;
 const { Text, Title } = Typography;
 
 
-const AddUser = (props) => {
+const AddUserModal = (props) => {
   const { t } = useTranslation();
   const { t } = useTranslation();
   const formApiRef = useRef(null);
   const formApiRef = useRef(null);
   const [loading, setLoading] = useState(false);
   const [loading, setLoading] = useState(false);
@@ -164,4 +164,4 @@ const AddUser = (props) => {
   );
   );
 };
 };
 
 
-export default AddUser;
+export default AddUserModal; 

+ 39 - 0
web/src/components/table/users/modals/DeleteUserModal.jsx

@@ -0,0 +1,39 @@
+import React from 'react';
+import { Modal } from '@douyinfe/semi-ui';
+
+const DeleteUserModal = ({ 
+  visible, 
+  onCancel, 
+  onConfirm, 
+  user,
+  users,
+  activePage,
+  refresh,
+  manageUser,
+  t 
+}) => {
+  const handleConfirm = async () => {
+    await manageUser(user.id, 'delete', user);
+    await refresh();
+    setTimeout(() => {
+      if (users.length === 0 && activePage > 1) {
+        refresh(activePage - 1);
+      }
+    }, 100);
+    onCancel(); // Close modal after success
+  };
+
+  return (
+    <Modal
+      title={t('确定是否要注销此用户?')}
+      visible={visible}
+      onCancel={onCancel}
+      onOk={handleConfirm}
+      type="danger"
+    >
+      {t('相当于删除用户,此修改将不可逆')}
+    </Modal>
+  );
+};
+
+export default DeleteUserModal; 

+ 18 - 0
web/src/components/table/users/modals/DemoteUserModal.jsx

@@ -0,0 +1,18 @@
+import React from 'react';
+import { Modal } from '@douyinfe/semi-ui';
+
+const DemoteUserModal = ({ visible, onCancel, onConfirm, user, t }) => {
+  return (
+    <Modal
+      title={t('确定要降级此用户吗?')}
+      visible={visible}
+      onCancel={onCancel}
+      onOk={onConfirm}
+      type="warning"
+    >
+      {t('此操作将降低用户的权限级别')}
+    </Modal>
+  );
+};
+
+export default DemoteUserModal; 

+ 4 - 4
web/src/pages/User/EditUser.js → web/src/components/table/users/modals/EditUserModal.jsx

@@ -6,8 +6,8 @@ import {
   showSuccess,
   showSuccess,
   renderQuota,
   renderQuota,
   renderQuotaWithPrompt,
   renderQuotaWithPrompt,
-} from '../../helpers';
-import { useIsMobile } from '../../hooks/common/useIsMobile.js';
+} from '../../../../helpers';
+import { useIsMobile } from '../../../../hooks/common/useIsMobile.js';
 import {
 import {
   Button,
   Button,
   Modal,
   Modal,
@@ -35,7 +35,7 @@ import {
 
 
 const { Text, Title } = Typography;
 const { Text, Title } = Typography;
 
 
-const EditUser = (props) => {
+const EditUserModal = (props) => {
   const { t } = useTranslation();
   const { t } = useTranslation();
   const userId = props.editingUser.id;
   const userId = props.editingUser.id;
   const [loading, setLoading] = useState(true);
   const [loading, setLoading] = useState(true);
@@ -348,4 +348,4 @@ const EditUser = (props) => {
   );
   );
 };
 };
 
 
-export default EditUser;
+export default EditUserModal; 

+ 27 - 0
web/src/components/table/users/modals/EnableDisableUserModal.jsx

@@ -0,0 +1,27 @@
+import React from 'react';
+import { Modal } from '@douyinfe/semi-ui';
+
+const EnableDisableUserModal = ({ 
+  visible, 
+  onCancel, 
+  onConfirm, 
+  user, 
+  action,
+  t 
+}) => {
+  const isDisable = action === 'disable';
+  
+  return (
+    <Modal
+      title={isDisable ? t('确定要禁用此用户吗?') : t('确定要启用此用户吗?')}
+      visible={visible}
+      onCancel={onCancel}
+      onOk={onConfirm}
+      type="warning"
+    >
+      {isDisable ? t('此操作将禁用用户账户') : t('此操作将启用用户账户')}
+    </Modal>
+  );
+};
+
+export default EnableDisableUserModal; 

+ 18 - 0
web/src/components/table/users/modals/PromoteUserModal.jsx

@@ -0,0 +1,18 @@
+import React from 'react';
+import { Modal } from '@douyinfe/semi-ui';
+
+const PromoteUserModal = ({ visible, onCancel, onConfirm, user, t }) => {
+  return (
+    <Modal
+      title={t('确定要提升此用户吗?')}
+      visible={visible}
+      onCancel={onCancel}
+      onOk={onConfirm}
+      type="warning"
+    >
+      {t('此操作将提升用户的权限级别')}
+    </Modal>
+  );
+};
+
+export default PromoteUserModal; 

+ 259 - 0
web/src/hooks/users/useUsersData.js

@@ -0,0 +1,259 @@
+import { useState, useEffect } from 'react';
+import { useTranslation } from 'react-i18next';
+import { API, showError, showSuccess } from '../../helpers';
+import { ITEMS_PER_PAGE } from '../../constants';
+import { useTableCompactMode } from '../common/useTableCompactMode';
+
+export const useUsersData = () => {
+  const { t } = useTranslation();
+  const [compactMode, setCompactMode] = useTableCompactMode('users');
+
+  // State management
+  const [users, setUsers] = useState([]);
+  const [loading, setLoading] = useState(true);
+  const [activePage, setActivePage] = useState(1);
+  const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
+  const [searching, setSearching] = useState(false);
+  const [groupOptions, setGroupOptions] = useState([]);
+  const [userCount, setUserCount] = useState(ITEMS_PER_PAGE);
+
+  // Modal states
+  const [showAddUser, setShowAddUser] = useState(false);
+  const [showEditUser, setShowEditUser] = useState(false);
+  const [editingUser, setEditingUser] = useState({
+    id: undefined,
+  });
+
+  // Form initial values
+  const formInitValues = {
+    searchKeyword: '',
+    searchGroup: '',
+  };
+
+  // Form API reference
+  const [formApi, setFormApi] = useState(null);
+
+  // Get form values helper function
+  const getFormValues = () => {
+    const formValues = formApi ? formApi.getValues() : {};
+    return {
+      searchKeyword: formValues.searchKeyword || '',
+      searchGroup: formValues.searchGroup || '',
+    };
+  };
+
+  // Set user format with key field
+  const setUserFormat = (users) => {
+    for (let i = 0; i < users.length; i++) {
+      users[i].key = users[i].id;
+    }
+    setUsers(users);
+  };
+
+  // Load users data
+  const loadUsers = async (startIdx, pageSize) => {
+    const res = await API.get(`/api/user/?p=${startIdx}&page_size=${pageSize}`);
+    const { success, message, data } = res.data;
+    if (success) {
+      const newPageData = data.items;
+      setActivePage(data.page);
+      setUserCount(data.total);
+      setUserFormat(newPageData);
+    } else {
+      showError(message);
+    }
+    setLoading(false);
+  };
+
+  // Search users with keyword and group
+  const searchUsers = async (
+    startIdx,
+    pageSize,
+    searchKeyword = null,
+    searchGroup = null,
+  ) => {
+    // If no parameters passed, get values from form
+    if (searchKeyword === null || searchGroup === null) {
+      const formValues = getFormValues();
+      searchKeyword = formValues.searchKeyword;
+      searchGroup = formValues.searchGroup;
+    }
+
+    if (searchKeyword === '' && searchGroup === '') {
+      // If keyword is blank, load files instead
+      await loadUsers(startIdx, pageSize);
+      return;
+    }
+    setSearching(true);
+    const res = await API.get(
+      `/api/user/search?keyword=${searchKeyword}&group=${searchGroup}&p=${startIdx}&page_size=${pageSize}`,
+    );
+    const { success, message, data } = res.data;
+    if (success) {
+      const newPageData = data.items;
+      setActivePage(data.page);
+      setUserCount(data.total);
+      setUserFormat(newPageData);
+    } else {
+      showError(message);
+    }
+    setSearching(false);
+  };
+
+  // Manage user operations (promote, demote, enable, disable, delete)
+  const manageUser = async (userId, action, record) => {
+    const res = await API.post('/api/user/manage', {
+      id: userId,
+      action,
+    });
+    const { success, message } = res.data;
+    if (success) {
+      showSuccess('操作成功完成!');
+      let user = res.data.data;
+      let newUsers = [...users];
+      if (action === 'delete') {
+        // Mark as deleted
+        const index = newUsers.findIndex(u => u.id === userId);
+        if (index > -1) {
+          newUsers[index].DeletedAt = new Date();
+        }
+      } else {
+        // Update status and role
+        record.status = user.status;
+        record.role = user.role;
+      }
+      setUsers(newUsers);
+    } else {
+      showError(message);
+    }
+  };
+
+  // Handle page change
+  const handlePageChange = (page) => {
+    setActivePage(page);
+    const { searchKeyword, searchGroup } = getFormValues();
+    if (searchKeyword === '' && searchGroup === '') {
+      loadUsers(page, pageSize).then();
+    } else {
+      searchUsers(page, pageSize, searchKeyword, searchGroup).then();
+    }
+  };
+
+  // Handle page size change
+  const handlePageSizeChange = async (size) => {
+    localStorage.setItem('page-size', size + '');
+    setPageSize(size);
+    setActivePage(1);
+    loadUsers(activePage, size)
+      .then()
+      .catch((reason) => {
+        showError(reason);
+      });
+  };
+
+  // Handle table row styling for disabled/deleted users
+  const handleRow = (record, index) => {
+    if (record.DeletedAt !== null || record.status !== 1) {
+      return {
+        style: {
+          background: 'var(--semi-color-disabled-border)',
+        },
+      };
+    } else {
+      return {};
+    }
+  };
+
+  // Refresh data
+  const refresh = async (page = activePage) => {
+    const { searchKeyword, searchGroup } = getFormValues();
+    if (searchKeyword === '' && searchGroup === '') {
+      await loadUsers(page, pageSize);
+    } else {
+      await searchUsers(page, pageSize, searchKeyword, searchGroup);
+    }
+  };
+
+  // Fetch groups data
+  const fetchGroups = async () => {
+    try {
+      let res = await API.get(`/api/group/`);
+      if (res === undefined) {
+        return;
+      }
+      setGroupOptions(
+        res.data.data.map((group) => ({
+          label: group,
+          value: group,
+        })),
+      );
+    } catch (error) {
+      showError(error.message);
+    }
+  };
+
+  // Modal control functions
+  const closeAddUser = () => {
+    setShowAddUser(false);
+  };
+
+  const closeEditUser = () => {
+    setShowEditUser(false);
+    setEditingUser({
+      id: undefined,
+    });
+  };
+
+  // Initialize data on component mount
+  useEffect(() => {
+    loadUsers(0, pageSize)
+      .then()
+      .catch((reason) => {
+        showError(reason);
+      });
+    fetchGroups().then();
+  }, []);
+
+  return {
+    // Data state
+    users,
+    loading,
+    activePage,
+    pageSize,
+    userCount,
+    searching,
+    groupOptions,
+
+    // Modal state
+    showAddUser,
+    showEditUser,
+    editingUser,
+    setShowAddUser,
+    setShowEditUser,
+    setEditingUser,
+
+    // Form state
+    formInitValues,
+    formApi,
+    setFormApi,
+
+    // UI state
+    compactMode,
+    setCompactMode,
+
+    // Actions
+    loadUsers,
+    searchUsers,
+    manageUser,
+    handlePageChange,
+    handlePageSizeChange,
+    handleRow,
+    refresh,
+    closeAddUser,
+    closeEditUser,
+    getFormValues,
+
+    // Translation
+    t,
+  };
+}; 

+ 2 - 2
web/src/pages/User/index.js

@@ -1,10 +1,10 @@
 import React from 'react';
 import React from 'react';
-import UsersTable from '../../components/table/UsersTable';
+import UsersPage from '../../components/table/users';
 
 
 const User = () => {
 const User = () => {
   return (
   return (
     <div className="mt-[60px] px-2">
     <div className="mt-[60px] px-2">
-      <UsersTable />
+      <UsersPage />
     </div>
     </div>
   );
   );
 };
 };