diff --git a/web/src/App.js b/web/src/App.js index 995ae2bb..41ab040e 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -7,7 +7,7 @@ import RegisterForm from './components/auth/RegisterForm.js'; import LoginForm from './components/auth/LoginForm.js'; import NotFound from './pages/NotFound'; import Setting from './pages/Setting'; -import EditUser from './pages/User/EditUser'; + import PasswordResetForm from './components/auth/PasswordResetForm.js'; import PasswordResetConfirm from './components/auth/PasswordResetConfirm.js'; import Channel from './pages/Channel'; @@ -109,22 +109,6 @@ function App() { } /> - } key={location.pathname}> - - - } - /> - } key={location.pathname}> - - - } - /> { - const { t } = useTranslation(); - const [compactMode, setCompactMode] = useTableCompactMode('users'); - - function renderRole(role) { - switch (role) { - case 1: - return ( - }> - {t('普通用户')} - - ); - case 10: - return ( - }> - {t('管理员')} - - ); - case 100: - return ( - }> - {t('超级管理员')} - - ); - default: - return ( - }> - {t('未知身份')} - - ); - } - } - - const renderStatus = (status) => { - switch (status) { - case 1: - return }>{t('已激活')}; - case 2: - return ( - }> - {t('已封禁')} - - ); - default: - return ( - }> - {t('未知状态')} - - ); - } - }; - - const columns = [ - { - title: 'ID', - dataIndex: 'id', - }, - { - title: t('用户名'), - dataIndex: 'username', - render: (text, record) => { - const remark = record.remark; - if (!remark) { - return {text}; - } - const maxLen = 10; - const displayRemark = remark.length > maxLen ? remark.slice(0, maxLen) + '…' : remark; - return ( - - {text} - - -
-
- {displayRemark} -
- - - - ); - }, - }, - { - title: t('分组'), - dataIndex: 'group', - render: (text, record, index) => { - return
{renderGroup(text)}
; - }, - }, - { - title: t('统计信息'), - dataIndex: 'info', - render: (text, record, index) => { - return ( -
- - }> - {t('剩余')}: {renderQuota(record.quota)} - - }> - {t('已用')}: {renderQuota(record.used_quota)} - - }> - {t('调用')}: {renderNumber(record.request_count)} - - -
- ); - }, - }, - { - title: t('邀请信息'), - dataIndex: 'invite', - render: (text, record, index) => { - return ( -
- - }> - {t('邀请')}: {renderNumber(record.aff_count)} - - }> - {t('收益')}: {renderQuota(record.aff_history_quota)} - - }> - {record.inviter_id === 0 ? t('无邀请人') : `邀请人: ${record.inviter_id}`} - - -
- ); - }, - }, - { - title: t('角色'), - dataIndex: 'role', - render: (text, record, index) => { - return
{renderRole(text)}
; - }, - }, - { - title: t('状态'), - dataIndex: 'status', - render: (text, record, index) => { - return ( -
- {record.DeletedAt !== null ? ( - }>{t('已注销')} - ) : ( - renderStatus(text) - )} -
- ); - }, - }, - { - 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 ( - - - - -
- } - actionsArea={ -
-
- -
- -
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" - > -
-
- } - placeholder={t('支持搜索用户的 ID、用户名、显示名称和邮箱地址')} - showClear - pure - size="small" - /> -
-
- { - // 分组变化时自动搜索 - setTimeout(() => { - setActivePage(1); - searchUsers(1, pageSize); - }, 100); - }} - className="w-full" - showClear - pure - size="small" - /> -
-
- - -
-
-
-
- } - > - 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={ - } - darkModeImage={} - description={t('搜索无结果')} - style={{ padding: 30 }} - /> - } - className="overflow-hidden" - size="middle" - /> - - - ); -}; - -export default UsersTable; diff --git a/web/src/components/table/users/UsersActions.jsx b/web/src/components/table/users/UsersActions.jsx new file mode 100644 index 00000000..c486cedc --- /dev/null +++ b/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 ( +
+ +
+ ); +}; + +export default UsersActions; \ No newline at end of file diff --git a/web/src/components/table/users/UsersColumnDefs.js b/web/src/components/table/users/UsersColumnDefs.js new file mode 100644 index 00000000..8c8bd5ac --- /dev/null +++ b/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 ( + }> + {t('普通用户')} + + ); + case 10: + return ( + }> + {t('管理员')} + + ); + case 100: + return ( + }> + {t('超级管理员')} + + ); + default: + return ( + }> + {t('未知身份')} + + ); + } +}; + +/** + * Render user status + */ +const renderStatus = (status, t) => { + switch (status) { + case 1: + return }>{t('已激活')}; + case 2: + return ( + }> + {t('已封禁')} + + ); + default: + return ( + }> + {t('未知状态')} + + ); + } +}; + +/** + * Render username with remark + */ +const renderUsername = (text, record) => { + const remark = record.remark; + if (!remark) { + return {text}; + } + const maxLen = 10; + const displayRemark = remark.length > maxLen ? remark.slice(0, maxLen) + '…' : remark; + return ( + + {text} + + +
+
+ {displayRemark} +
+ + + + ); +}; + +/** + * Render user statistics + */ +const renderStatistics = (text, record, t) => { + return ( +
+ + }> + {t('剩余')}: {renderQuota(record.quota)} + + }> + {t('已用')}: {renderQuota(record.used_quota)} + + }> + {t('调用')}: {renderNumber(record.request_count)} + + +
+ ); +}; + +/** + * Render invite information + */ +const renderInviteInfo = (text, record, t) => { + return ( +
+ + }> + {t('邀请')}: {renderNumber(record.aff_count)} + + }> + {t('收益')}: {renderQuota(record.aff_history_quota)} + + }> + {record.inviter_id === 0 ? t('无邀请人') : `邀请人: ${record.inviter_id}`} + + +
+ ); +}; + +/** + * Render overall status including deleted status + */ +const renderOverallStatus = (status, record, t) => { + if (record.DeletedAt !== null) { + return }>{t('已注销')}; + } 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 ( + + + + +
+ ); +}; + +export default UsersDescription; \ No newline at end of file diff --git a/web/src/components/table/users/UsersFilters.jsx b/web/src/components/table/users/UsersFilters.jsx new file mode 100644 index 00000000..201b1d1a --- /dev/null +++ b/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 ( +
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" + > +
+
+ } + placeholder={t('支持搜索用户的 ID、用户名、显示名称和邮箱地址')} + showClear + pure + size="small" + /> +
+
+ { + // Group change triggers automatic search + setTimeout(() => { + searchUsers(1, pageSize); + }, 100); + }} + className="w-full" + showClear + pure + size="small" + /> +
+
+ + +
+
+ + ); +}; + +export default UsersFilters; \ No newline at end of file diff --git a/web/src/components/table/users/UsersTable.jsx b/web/src/components/table/users/UsersTable.jsx new file mode 100644 index 00000000..459145fb --- /dev/null +++ b/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 ( + <> +
} + darkModeImage={} + description={t('搜索无结果')} + style={{ padding: 30 }} + /> + } + className="overflow-hidden" + size="middle" + /> + + {/* Modal components */} + setShowPromoteModal(false)} + onConfirm={handlePromoteConfirm} + user={modalUser} + t={t} + /> + + setShowDemoteModal(false)} + onConfirm={handleDemoteConfirm} + user={modalUser} + t={t} + /> + + setShowEnableDisableModal(false)} + onConfirm={handleEnableDisableConfirm} + user={modalUser} + action={enableDisableAction} + t={t} + /> + + setShowDeleteModal(false)} + user={modalUser} + users={users} + activePage={activePage} + refresh={refresh} + manageUser={manageUser} + t={t} + /> + + ); +}; + +export default UsersTable; \ No newline at end of file diff --git a/web/src/components/table/users/index.jsx b/web/src/components/table/users/index.jsx new file mode 100644 index 00000000..5eba39a6 --- /dev/null +++ b/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 ( + <> + + + + + + } + actionsArea={ +
+ + + +
+ } + > + +
+ + ); +}; + +export default UsersPage; \ No newline at end of file diff --git a/web/src/pages/User/AddUser.js b/web/src/components/table/users/modals/AddUserModal.jsx similarity index 95% rename from web/src/pages/User/AddUser.js rename to web/src/components/table/users/modals/AddUserModal.jsx index 54d9b002..59df7ef7 100644 --- a/web/src/pages/User/AddUser.js +++ b/web/src/components/table/users/modals/AddUserModal.jsx @@ -1,6 +1,6 @@ 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 { Button, SideSheet, @@ -23,7 +23,7 @@ import { useTranslation } from 'react-i18next'; const { Text, Title } = Typography; -const AddUser = (props) => { +const AddUserModal = (props) => { const { t } = useTranslation(); const formApiRef = useRef(null); const [loading, setLoading] = useState(false); @@ -164,4 +164,4 @@ const AddUser = (props) => { ); }; -export default AddUser; +export default AddUserModal; \ No newline at end of file diff --git a/web/src/components/table/users/modals/DeleteUserModal.jsx b/web/src/components/table/users/modals/DeleteUserModal.jsx new file mode 100644 index 00000000..8ba89d90 --- /dev/null +++ b/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 ( + + {t('相当于删除用户,此修改将不可逆')} + + ); +}; + +export default DeleteUserModal; \ No newline at end of file diff --git a/web/src/components/table/users/modals/DemoteUserModal.jsx b/web/src/components/table/users/modals/DemoteUserModal.jsx new file mode 100644 index 00000000..c3885ebf --- /dev/null +++ b/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 ( + + {t('此操作将降低用户的权限级别')} + + ); +}; + +export default DemoteUserModal; \ No newline at end of file diff --git a/web/src/pages/User/EditUser.js b/web/src/components/table/users/modals/EditUserModal.jsx similarity index 98% rename from web/src/pages/User/EditUser.js rename to web/src/components/table/users/modals/EditUserModal.jsx index 53fa9b20..330f4702 100644 --- a/web/src/pages/User/EditUser.js +++ b/web/src/components/table/users/modals/EditUserModal.jsx @@ -6,8 +6,8 @@ import { showSuccess, renderQuota, renderQuotaWithPrompt, -} from '../../helpers'; -import { useIsMobile } from '../../hooks/common/useIsMobile.js'; +} from '../../../../helpers'; +import { useIsMobile } from '../../../../hooks/common/useIsMobile.js'; import { Button, Modal, @@ -35,7 +35,7 @@ import { const { Text, Title } = Typography; -const EditUser = (props) => { +const EditUserModal = (props) => { const { t } = useTranslation(); const userId = props.editingUser.id; const [loading, setLoading] = useState(true); @@ -348,4 +348,4 @@ const EditUser = (props) => { ); }; -export default EditUser; +export default EditUserModal; \ No newline at end of file diff --git a/web/src/components/table/users/modals/EnableDisableUserModal.jsx b/web/src/components/table/users/modals/EnableDisableUserModal.jsx new file mode 100644 index 00000000..be95cf40 --- /dev/null +++ b/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 ( + + {isDisable ? t('此操作将禁用用户账户') : t('此操作将启用用户账户')} + + ); +}; + +export default EnableDisableUserModal; \ No newline at end of file diff --git a/web/src/components/table/users/modals/PromoteUserModal.jsx b/web/src/components/table/users/modals/PromoteUserModal.jsx new file mode 100644 index 00000000..0a47d15a --- /dev/null +++ b/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 ( + + {t('此操作将提升用户的权限级别')} + + ); +}; + +export default PromoteUserModal; \ No newline at end of file diff --git a/web/src/hooks/users/useUsersData.js b/web/src/hooks/users/useUsersData.js new file mode 100644 index 00000000..a9952a76 --- /dev/null +++ b/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, + }; +}; \ No newline at end of file diff --git a/web/src/pages/User/index.js b/web/src/pages/User/index.js index 12b6f4ee..d06ee7ed 100644 --- a/web/src/pages/User/index.js +++ b/web/src/pages/User/index.js @@ -1,10 +1,10 @@ import React from 'react'; -import UsersTable from '../../components/table/UsersTable'; +import UsersPage from '../../components/table/users'; const User = () => { return (
- +
); };