mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-05-31 18:24:10 +00:00
feat(clients): tidier bulk action toolbar
When at least one client is selected, the toolbar now collapses to a
small selection indicator plus the three most-used actions instead of
spreading six count-suffixed buttons across the row:
- Replaces every per-button "(N)" with a single closable "{N} selected"
tag on the left — one click on its × clears the selection.
- Hides "+ Add Clients" while a selection is active (focus mode).
- Keeps Attach, Detach, and Delete as visible buttons; Delete is pushed
to the right with auto margin so it doesn't sit flush against the
non-destructive actions.
- Folds Adjust, Group, and Sub links into the existing "more"
dropdown, which is now context-aware: selection-scoped overflow when
rows are picked, global actions (Add Bulk / Reset all / Del depleted)
otherwise.
On mobile the new buttons collapse to icon-only the same way as the
rest of the toolbar.
This commit is contained in:
parent
8d6d845262
commit
bf1b488a63
3 changed files with 75 additions and 39 deletions
|
|
@ -783,28 +783,25 @@ export default function ClientsPage() {
|
||||||
hoverable
|
hoverable
|
||||||
title={
|
title={
|
||||||
<div className="card-toolbar">
|
<div className="card-toolbar">
|
||||||
<Button type="primary" icon={<PlusOutlined />} onClick={onAdd}>
|
{selectedRowKeys.length === 0 ? (
|
||||||
{!isMobile && t('pages.clients.addClients')}
|
<Button type="primary" icon={<PlusOutlined />} onClick={onAdd}>
|
||||||
</Button>
|
{!isMobile && t('pages.clients.addClients')}
|
||||||
{selectedRowKeys.length > 0 && (
|
</Button>
|
||||||
|
) : (
|
||||||
<>
|
<>
|
||||||
<Button icon={<ClockCircleOutlined />} onClick={() => setBulkAdjustOpen(true)}>
|
<Tag
|
||||||
{t('pages.clients.adjustSelected', { count: selectedRowKeys.length })}
|
color="blue"
|
||||||
</Button>
|
closable
|
||||||
<Button icon={<TagsOutlined />} onClick={() => setBulkGroupOpen(true)}>
|
onClose={() => setSelectedRowKeys([])}
|
||||||
{t('pages.clients.assignGroupSelected', { count: selectedRowKeys.length })}
|
style={{ marginInlineEnd: 0, padding: '4px 8px', fontSize: 13 }}
|
||||||
</Button>
|
>
|
||||||
|
{t('pages.clients.selectedCount', { count: selectedRowKeys.length })}
|
||||||
|
</Tag>
|
||||||
<Button icon={<UsergroupAddOutlined />} onClick={() => setBulkAttachOpen(true)}>
|
<Button icon={<UsergroupAddOutlined />} onClick={() => setBulkAttachOpen(true)}>
|
||||||
{t('pages.clients.attachSelected', { count: selectedRowKeys.length })}
|
{!isMobile && t('pages.clients.attach')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button danger icon={<UsergroupDeleteOutlined />} onClick={() => setBulkDetachOpen(true)}>
|
<Button danger icon={<UsergroupDeleteOutlined />} onClick={() => setBulkDetachOpen(true)}>
|
||||||
{t('pages.clients.detachSelected', { count: selectedRowKeys.length })}
|
{!isMobile && t('pages.clients.detach')}
|
||||||
</Button>
|
|
||||||
<Button icon={<LinkOutlined />} onClick={() => setSubLinksOpen(true)}>
|
|
||||||
{t('pages.clients.subLinksSelected', { count: selectedRowKeys.length })}
|
|
||||||
</Button>
|
|
||||||
<Button danger icon={<DeleteOutlined />} onClick={onBulkDelete}>
|
|
||||||
{t('pages.clients.deleteSelected', { count: selectedRowKeys.length })}
|
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
@ -812,33 +809,64 @@ export default function ClientsPage() {
|
||||||
trigger={['click']}
|
trigger={['click']}
|
||||||
placement="bottomRight"
|
placement="bottomRight"
|
||||||
menu={{
|
menu={{
|
||||||
items: [
|
items: selectedRowKeys.length > 0
|
||||||
{
|
? [
|
||||||
key: 'bulk',
|
{
|
||||||
icon: <UsergroupAddOutlined />,
|
key: 'adjust',
|
||||||
label: t('pages.clients.bulk'),
|
icon: <ClockCircleOutlined />,
|
||||||
onClick: () => setBulkAddOpen(true),
|
label: t('pages.clients.adjust'),
|
||||||
},
|
onClick: () => setBulkAdjustOpen(true),
|
||||||
{
|
},
|
||||||
key: 'resetAll',
|
{
|
||||||
icon: <RetweetOutlined />,
|
key: 'group',
|
||||||
label: t('pages.clients.resetAllTraffics'),
|
icon: <TagsOutlined />,
|
||||||
onClick: onResetAllTraffics,
|
label: t('pages.clients.group'),
|
||||||
},
|
onClick: () => setBulkGroupOpen(true),
|
||||||
{
|
},
|
||||||
key: 'delDepleted',
|
{
|
||||||
icon: <RestOutlined />,
|
key: 'subLinks',
|
||||||
label: t('pages.clients.delDepleted'),
|
icon: <LinkOutlined />,
|
||||||
danger: true,
|
label: t('pages.clients.subLinks'),
|
||||||
onClick: onDelDepleted,
|
onClick: () => setSubLinksOpen(true),
|
||||||
},
|
},
|
||||||
],
|
]
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
key: 'bulk',
|
||||||
|
icon: <UsergroupAddOutlined />,
|
||||||
|
label: t('pages.clients.bulk'),
|
||||||
|
onClick: () => setBulkAddOpen(true),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'resetAll',
|
||||||
|
icon: <RetweetOutlined />,
|
||||||
|
label: t('pages.clients.resetAllTraffics'),
|
||||||
|
onClick: onResetAllTraffics,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'delDepleted',
|
||||||
|
icon: <RestOutlined />,
|
||||||
|
label: t('pages.clients.delDepleted'),
|
||||||
|
danger: true,
|
||||||
|
onClick: onDelDepleted,
|
||||||
|
},
|
||||||
|
],
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button icon={<MoreOutlined />}>
|
<Button icon={<MoreOutlined />}>
|
||||||
{!isMobile && t('more')}
|
{!isMobile && t('more')}
|
||||||
</Button>
|
</Button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
{selectedRowKeys.length > 0 && (
|
||||||
|
<Button
|
||||||
|
danger
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
onClick={onBulkDelete}
|
||||||
|
style={{ marginInlineStart: 'auto' }}
|
||||||
|
>
|
||||||
|
{!isMobile && t('delete')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -542,6 +542,10 @@
|
||||||
"assignGroupPlaceholder": "Group name (leave blank to clear)",
|
"assignGroupPlaceholder": "Group name (leave blank to clear)",
|
||||||
"assignGroupAssignedToast": "Assigned {count} client(s) to {group}",
|
"assignGroupAssignedToast": "Assigned {count} client(s) to {group}",
|
||||||
"assignGroupClearedToast": "Cleared group from {count} client(s)",
|
"assignGroupClearedToast": "Cleared group from {count} client(s)",
|
||||||
|
"attach": "Attach",
|
||||||
|
"adjust": "Adjust",
|
||||||
|
"subLinks": "Sub links",
|
||||||
|
"selectedCount": "{count} selected",
|
||||||
"attachSelected": "Attach ({count})",
|
"attachSelected": "Attach ({count})",
|
||||||
"attachToInboundsTitle": "Attach {count} client(s) to inbound(s)",
|
"attachToInboundsTitle": "Attach {count} client(s) to inbound(s)",
|
||||||
"attachToInboundsDesc": "Attaches the selected {count} client(s) (same UUID/password and shared traffic) to the chosen inbound(s). They keep their existing attachments too.",
|
"attachToInboundsDesc": "Attaches the selected {count} client(s) (same UUID/password and shared traffic) to the chosen inbound(s). They keep their existing attachments too.",
|
||||||
|
|
|
||||||
|
|
@ -513,6 +513,10 @@
|
||||||
"deleteConfirmContent": "این کلاینت از تمام اینباندهای متصل حذف و سابقه ترافیک آن پاک میشود. این عمل غیرقابل بازگشت است.",
|
"deleteConfirmContent": "این کلاینت از تمام اینباندهای متصل حذف و سابقه ترافیک آن پاک میشود. این عمل غیرقابل بازگشت است.",
|
||||||
"deleteSelected": "حذف ({count})",
|
"deleteSelected": "حذف ({count})",
|
||||||
"adjustSelected": "تنظیم ({count})",
|
"adjustSelected": "تنظیم ({count})",
|
||||||
|
"attach": "اتصال",
|
||||||
|
"adjust": "تنظیم",
|
||||||
|
"subLinks": "لینکهای ساب",
|
||||||
|
"selectedCount": "{count} انتخابشده",
|
||||||
"attachSelected": "اتصال ({count})",
|
"attachSelected": "اتصال ({count})",
|
||||||
"attachToInboundsTitle": "اتصال {count} کلاینت به اینباند(ها)",
|
"attachToInboundsTitle": "اتصال {count} کلاینت به اینباند(ها)",
|
||||||
"attachToInboundsDesc": "{count} کلاینت انتخابشده (با همان UUID/پسورد و ترافیک مشترک) به اینباند(های) انتخابی متصل میشوند. روی اینباندهای فعلی هم باقی میمانند.",
|
"attachToInboundsDesc": "{count} کلاینت انتخابشده (با همان UUID/پسورد و ترافیک مشترک) به اینباند(های) انتخابی متصل میشوند. روی اینباندهای فعلی هم باقی میمانند.",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue