From 39079e7affcb72665a5d4e7fe1e1c19896eccb9e Mon Sep 17 00:00:00 2001 From: t0ng7u Date: Sun, 20 Jul 2025 01:00:53 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=92=84=20refactor:=20Users=20table=20UI?= =?UTF-8?q?=20&=20state=20handling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary of changes 1. UI clean-up • Removed all `prefixIcon` props from `Tag` components in `UsersColumnDefs.js`. • Corrected i18n string in invite info (`${t('邀请人')}: …`). 2. “Statistics” column overhaul • Added a Switch (enable / disable) and quota Progress bar, mirroring the Tokens table design. • Moved enable / disable action out of the “More” dropdown; user status is now toggled directly via the Switch. • Disabled the Switch for deleted (注销) users. • Restored column title to “Statistics” to avoid duplication. 3. State consistency / refresh • Updated `manageUser` in `useUsersData.js` to: – set `loading` while processing actions; – update users list immutably (new objects & array) to trigger React re-render. 4. Imports / plumbing • Added `Progress` and `Switch` to UI imports in `UsersColumnDefs.js`. These changes streamline the user table’s appearance, align interaction patterns with the token table, and ensure immediate visual feedback after user status changes. --- .../components/table/users/UsersColumnDefs.js | 258 ++++++++---------- web/src/hooks/users/useUsersData.js | 30 +- web/src/i18n/locales/en.json | 2 +- 3 files changed, 140 insertions(+), 150 deletions(-) diff --git a/web/src/components/table/users/UsersColumnDefs.js b/web/src/components/table/users/UsersColumnDefs.js index d668760b..774554cb 100644 --- a/web/src/components/table/users/UsersColumnDefs.js +++ b/web/src/components/table/users/UsersColumnDefs.js @@ -20,31 +20,14 @@ For commercial licensing, please contact support@quantumnous.com import React from 'react'; import { Button, - Dropdown, Space, Tag, Tooltip, - Typography + Progress, + Switch, } 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 */ @@ -52,53 +35,31 @@ 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 */ @@ -127,22 +88,91 @@ const renderUsername = (text, record) => { /** * Render user statistics */ -const renderStatistics = (text, record, t) => { - return ( -
- - }> - {t('剩余')}: {renderQuota(record.quota)} - - }> - {t('已用')}: {renderQuota(record.used_quota)} - - }> - {t('调用')}: {renderNumber(record.request_count)} - - +const renderStatistics = (text, record, showEnableDisableModal, t) => { + const enabled = record.status === 1; + const isDeleted = record.DeletedAt !== null; + + // Determine tag text & color like original status column + let tagColor = 'grey'; + let tagText = t('未知状态'); + if (isDeleted) { + tagColor = 'red'; + tagText = t('已注销'); + } else if (record.status === 1) { + tagColor = 'green'; + tagText = t('已激活'); + } else if (record.status === 2) { + tagColor = 'red'; + tagText = t('已封禁'); + } + + const handleToggle = (checked) => { + if (checked) { + showEnableDisableModal(record, 'enable'); + } else { + showEnableDisableModal(record, 'disable'); + } + }; + + const used = parseInt(record.used_quota) || 0; + const remain = parseInt(record.quota) || 0; + const total = used + remain; + const percent = total > 0 ? (remain / total) * 100 : 0; + + const getProgressColor = (pct) => { + if (pct === 100) return 'var(--semi-color-success)'; + if (pct <= 10) return 'var(--semi-color-danger)'; + if (pct <= 30) return 'var(--semi-color-warning)'; + return undefined; + }; + + const quotaSuffix = ( +
+ {`${renderQuota(remain)} / ${renderQuota(total)}`} + `${percent.toFixed(0)}%`} + style={{ width: '100%', marginTop: '1px', marginBottom: 0 }} + />
); + + const content = ( + + } + suffixIcon={quotaSuffix} + > + {tagText} + + ); + + const tooltipContent = ( +
+
{t('已用额度')}: {renderQuota(used)}
+
{t('剩余额度')}: {renderQuota(remain)} ({percent.toFixed(0)}%)
+
{t('总额度')}: {renderQuota(total)}
+
{t('调用次数')}: {renderNumber(record.request_count)}
+
+ ); + + return ( + + {content} + + ); }; /** @@ -152,31 +182,20 @@ 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}`} + + {record.inviter_id === 0 ? t('无邀请人') : `${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 */ @@ -185,7 +204,6 @@ const renderOperations = (text, record, { setShowEditUser, showPromoteModal, showDemoteModal, - showEnableDisableModal, showDeleteModal, t }) => { @@ -193,46 +211,6 @@ const renderOperations = (text, record, { 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 ( - showPromoteModal(record)} > - + + ); }; @@ -289,16 +277,6 @@ export const getUsersColumns = ({ return
{renderGroup(text)}
; }, }, - { - 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', @@ -308,13 +286,19 @@ export const getUsersColumns = ({ }, { title: t('状态'), - dataIndex: 'status', - render: (text, record, index) => renderOverallStatus(text, record, t), + dataIndex: 'info', + render: (text, record, index) => renderStatistics(text, record, showEnableDisableModal, t), + }, + { + title: t('邀请信息'), + dataIndex: 'invite', + render: (text, record, index) => renderInviteInfo(text, record, t), }, { title: '', dataIndex: 'operate', fixed: 'right', + width: 200, render: (text, record, index) => renderOperations(text, record, { setEditingUser, setShowEditUser, diff --git a/web/src/hooks/users/useUsersData.js b/web/src/hooks/users/useUsersData.js index 56211057..828c1118 100644 --- a/web/src/hooks/users/useUsersData.js +++ b/web/src/hooks/users/useUsersData.js @@ -121,30 +121,36 @@ export const useUsersData = () => { // Manage user operations (promote, demote, enable, disable, delete) const manageUser = async (userId, action, record) => { + // Trigger loading state to force table re-render + setLoading(true); + 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(); + const user = res.data.data; + + // Create a new array and new object to ensure React detects changes + const newUsers = users.map((u) => { + if (u.id === userId) { + if (action === 'delete') { + return { ...u, DeletedAt: new Date() }; + } + return { ...u, status: user.status, role: user.role }; } - } else { - // Update status and role - record.status = user.status; - record.role = user.role; - } + return u; + }); + setUsers(newUsers); } else { showError(message); } + + setLoading(false); }; // Handle page change diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index 23d1a5e8..92ad7bd7 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -390,7 +390,6 @@ "已封禁": "Banned", "搜索用户的 ID,用户名,显示名称,以及邮箱地址 ...": "Search user ID, username, display name, and email address...", "用户名": "Username", - "统计信息": "Statistics", "用户角色": "User Role", "未绑定邮箱地址": "Email not bound", "请求次数": "Number of Requests", @@ -1483,6 +1482,7 @@ "剩余": "Remaining", "已用": "Used", "调用": "Calls", + "调用次数": "Call Count", "邀请": "Invitations", "收益": "Earnings", "无邀请人": "No Inviter",