mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-25 04:17:15 +00:00
feat(clients): restore reset traffic button in edit client form
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
|||||||
Input,
|
Input,
|
||||||
InputNumber,
|
InputNumber,
|
||||||
Modal,
|
Modal,
|
||||||
|
Popconfirm,
|
||||||
Row,
|
Row,
|
||||||
Select,
|
Select,
|
||||||
Space,
|
Space,
|
||||||
@@ -17,7 +18,7 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
message,
|
message,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import { EyeOutlined, ReloadOutlined } from '@ant-design/icons';
|
import { EyeOutlined, ReloadOutlined, RetweetOutlined } from '@ant-design/icons';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import type { Dayjs } from 'dayjs';
|
import type { Dayjs } from 'dayjs';
|
||||||
import { HttpUtil, RandomUtil } from '@/utils';
|
import { HttpUtil, RandomUtil } from '@/utils';
|
||||||
@@ -39,6 +40,7 @@ const CLIENT_IP_LOG_MODAL_Z_INDEX = CLIENT_FORM_MODAL_Z_INDEX + 1;
|
|||||||
|
|
||||||
interface ApiMsg<T = unknown> {
|
interface ApiMsg<T = unknown> {
|
||||||
success?: boolean;
|
success?: boolean;
|
||||||
|
msg?: string;
|
||||||
obj?: T;
|
obj?: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,6 +74,7 @@ interface ClientFormModalProps {
|
|||||||
payload: Record<string, unknown> | SaveCreatePayload,
|
payload: Record<string, unknown> | SaveCreatePayload,
|
||||||
meta: SaveMetaEdit | SaveMetaCreate,
|
meta: SaveMetaEdit | SaveMetaCreate,
|
||||||
) => Promise<ApiMsg | null>;
|
) => Promise<ApiMsg | null>;
|
||||||
|
resetTraffic?: (client: ClientRecord) => Promise<ApiMsg | null>;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,6 +143,7 @@ export default function ClientFormModal({
|
|||||||
tgBotEnable = false,
|
tgBotEnable = false,
|
||||||
groups = [],
|
groups = [],
|
||||||
save,
|
save,
|
||||||
|
resetTraffic,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
}: ClientFormModalProps) {
|
}: ClientFormModalProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -148,6 +152,7 @@ export default function ClientFormModal({
|
|||||||
|
|
||||||
const [form, setForm] = useState<FormState>(emptyForm);
|
const [form, setForm] = useState<FormState>(emptyForm);
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
const [resetting, setResetting] = useState(false);
|
||||||
const [clientIps, setClientIps] = useState<string[]>([]);
|
const [clientIps, setClientIps] = useState<string[]>([]);
|
||||||
const [ipsLoading, setIpsLoading] = useState(false);
|
const [ipsLoading, setIpsLoading] = useState(false);
|
||||||
const [ipsClearing, setIpsClearing] = useState(false);
|
const [ipsClearing, setIpsClearing] = useState(false);
|
||||||
@@ -328,6 +333,21 @@ export default function ClientFormModal({
|
|||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function onResetTraffic() {
|
||||||
|
if (!isEdit || !client?.email || !resetTraffic) return;
|
||||||
|
setResetting(true);
|
||||||
|
try {
|
||||||
|
const msg = await resetTraffic(client);
|
||||||
|
if (msg?.success) {
|
||||||
|
messageApi.success(t('pages.clients.toasts.trafficReset'));
|
||||||
|
} else {
|
||||||
|
messageApi.error(msg?.msg || t('somethingWentWrong'));
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setResetting(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function onSubmit() {
|
async function onSubmit() {
|
||||||
const schema = isEdit ? ClientFormSchema : ClientCreateFormSchema;
|
const schema = isEdit ? ClientFormSchema : ClientCreateFormSchema;
|
||||||
const validated = schema.safeParse({
|
const validated = schema.safeParse({
|
||||||
@@ -413,15 +433,35 @@ export default function ClientFormModal({
|
|||||||
open={open}
|
open={open}
|
||||||
title={isEdit ? t('pages.clients.editClient') : t('pages.clients.addClient')}
|
title={isEdit ? t('pages.clients.editClient') : t('pages.clients.addClient')}
|
||||||
destroyOnHidden
|
destroyOnHidden
|
||||||
okText={isEdit ? t('save') : t('create')}
|
|
||||||
cancelText={t('cancel')}
|
|
||||||
okButtonProps={{ loading: submitting }}
|
|
||||||
width={720}
|
width={720}
|
||||||
zIndex={CLIENT_FORM_MODAL_Z_INDEX}
|
zIndex={CLIENT_FORM_MODAL_Z_INDEX}
|
||||||
style={{ top: 20 }}
|
style={{ top: 20 }}
|
||||||
styles={{ body: { maxHeight: 'calc(100vh - 160px)', overflowY: 'auto', overflowX: 'hidden' } }}
|
styles={{ body: { maxHeight: 'calc(100vh - 160px)', overflowY: 'auto', overflowX: 'hidden' } }}
|
||||||
onOk={onSubmit}
|
|
||||||
onCancel={close}
|
onCancel={close}
|
||||||
|
footer={
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
|
{isEdit && resetTraffic && (
|
||||||
|
<Popconfirm
|
||||||
|
title={t('pages.inbounds.resetTraffic')}
|
||||||
|
description={t('pages.inbounds.resetTrafficContent')}
|
||||||
|
okText={t('reset')}
|
||||||
|
cancelText={t('cancel')}
|
||||||
|
zIndex={CLIENT_IP_LOG_MODAL_Z_INDEX}
|
||||||
|
onConfirm={onResetTraffic}
|
||||||
|
>
|
||||||
|
<Button color="danger" variant="filled" icon={<RetweetOutlined />} loading={resetting}>
|
||||||
|
{t('pages.inbounds.resetTraffic')}
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
)}
|
||||||
|
<div style={{ marginInlineStart: 'auto', display: 'flex', gap: 8 }}>
|
||||||
|
<Button onClick={close}>{t('cancel')}</Button>
|
||||||
|
<Button type="primary" loading={submitting} onClick={onSubmit}>
|
||||||
|
{isEdit ? t('save') : t('create')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Form layout="vertical">
|
<Form layout="vertical">
|
||||||
<Tabs
|
<Tabs
|
||||||
|
|||||||
@@ -165,15 +165,15 @@ function gbToBytes(gb: number | undefined): number {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SORT_OPTIONS: { value: string; column: string; order: 'ascend' | 'descend'; labelKey: string }[] = [
|
const SORT_OPTIONS: { value: string; column: string; order: 'ascend' | 'descend'; labelKey: string }[] = [
|
||||||
{ value: 'createdAt:ascend', column: 'createdAt', order: 'ascend', labelKey: 'pages.clients.sortOldest' },
|
{ value: 'createdAt:ascend', column: 'createdAt', order: 'ascend', labelKey: 'pages.clients.sortOldest' },
|
||||||
{ value: 'createdAt:descend', column: 'createdAt', order: 'descend', labelKey: 'pages.clients.sortNewest' },
|
{ value: 'createdAt:descend', column: 'createdAt', order: 'descend', labelKey: 'pages.clients.sortNewest' },
|
||||||
{ value: 'updatedAt:descend', column: 'updatedAt', order: 'descend', labelKey: 'pages.clients.sortRecentlyUpdated' },
|
{ value: 'updatedAt:descend', column: 'updatedAt', order: 'descend', labelKey: 'pages.clients.sortRecentlyUpdated' },
|
||||||
{ value: 'lastOnline:descend', column: 'lastOnline', order: 'descend', labelKey: 'pages.clients.sortRecentlyOnline' },
|
{ value: 'lastOnline:descend', column: 'lastOnline', order: 'descend', labelKey: 'pages.clients.sortRecentlyOnline' },
|
||||||
{ value: 'email:ascend', column: 'email', order: 'ascend', labelKey: 'pages.clients.sortEmailAZ' },
|
{ value: 'email:ascend', column: 'email', order: 'ascend', labelKey: 'pages.clients.sortEmailAZ' },
|
||||||
{ value: 'email:descend', column: 'email', order: 'descend', labelKey: 'pages.clients.sortEmailZA' },
|
{ value: 'email:descend', column: 'email', order: 'descend', labelKey: 'pages.clients.sortEmailZA' },
|
||||||
{ value: 'traffic:descend', column: 'traffic', order: 'descend', labelKey: 'pages.clients.sortMostTraffic' },
|
{ value: 'traffic:descend', column: 'traffic', order: 'descend', labelKey: 'pages.clients.sortMostTraffic' },
|
||||||
{ value: 'remaining:descend', column: 'remaining', order: 'descend', labelKey: 'pages.clients.sortHighestRemaining' },
|
{ value: 'remaining:descend', column: 'remaining', order: 'descend', labelKey: 'pages.clients.sortHighestRemaining' },
|
||||||
{ value: 'expiryTime:ascend', column: 'expiryTime', order: 'ascend', labelKey: 'pages.clients.sortExpiringSoonest' },
|
{ value: 'expiryTime:ascend', column: 'expiryTime', order: 'ascend', labelKey: 'pages.clients.sortExpiringSoonest' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const DEFAULT_SORT = SORT_OPTIONS[0];
|
const DEFAULT_SORT = SORT_OPTIONS[0];
|
||||||
@@ -743,6 +743,7 @@ export default function ClientsPage() {
|
|||||||
{
|
{
|
||||||
title: t('pages.clients.traffic'),
|
title: t('pages.clients.traffic'),
|
||||||
key: 'traffic',
|
key: 'traffic',
|
||||||
|
width: 300,
|
||||||
render: (_v, record) => (
|
render: (_v, record) => (
|
||||||
<ClientTrafficCell
|
<ClientTrafficCell
|
||||||
up={record.traffic?.up}
|
up={record.traffic?.up}
|
||||||
@@ -924,40 +925,40 @@ export default function ClientsPage() {
|
|||||||
menu={{
|
menu={{
|
||||||
items: selectedRowKeys.length > 0
|
items: selectedRowKeys.length > 0
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
key: 'adjust',
|
key: 'adjust',
|
||||||
icon: <ClockCircleOutlined />,
|
icon: <ClockCircleOutlined />,
|
||||||
label: t('pages.clients.adjust'),
|
label: t('pages.clients.adjust'),
|
||||||
onClick: () => setBulkAdjustOpen(true),
|
onClick: () => setBulkAdjustOpen(true),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'subLinks',
|
key: 'subLinks',
|
||||||
icon: <LinkOutlined />,
|
icon: <LinkOutlined />,
|
||||||
label: t('pages.clients.subLinks'),
|
label: t('pages.clients.subLinks'),
|
||||||
onClick: () => setSubLinksOpen(true),
|
onClick: () => setSubLinksOpen(true),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
{
|
{
|
||||||
key: 'bulk',
|
key: 'bulk',
|
||||||
icon: <UsergroupAddOutlined />,
|
icon: <UsergroupAddOutlined />,
|
||||||
label: t('pages.clients.bulk'),
|
label: t('pages.clients.bulk'),
|
||||||
onClick: () => setBulkAddOpen(true),
|
onClick: () => setBulkAddOpen(true),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'resetAll',
|
key: 'resetAll',
|
||||||
icon: <RetweetOutlined />,
|
icon: <RetweetOutlined />,
|
||||||
label: t('pages.clients.resetAllTraffics'),
|
label: t('pages.clients.resetAllTraffics'),
|
||||||
onClick: onResetAllTraffics,
|
onClick: onResetAllTraffics,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'delDepleted',
|
key: 'delDepleted',
|
||||||
icon: <RestOutlined />,
|
icon: <RestOutlined />,
|
||||||
label: t('pages.clients.delDepleted'),
|
label: t('pages.clients.delDepleted'),
|
||||||
danger: true,
|
danger: true,
|
||||||
onClick: onDelDepleted,
|
onClick: onDelDepleted,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button icon={<MoreOutlined />}>
|
<Button icon={<MoreOutlined />}>
|
||||||
@@ -1246,6 +1247,7 @@ export default function ClientsPage() {
|
|||||||
tgBotEnable={tgBotEnable}
|
tgBotEnable={tgBotEnable}
|
||||||
groups={allGroups}
|
groups={allGroups}
|
||||||
save={onSave}
|
save={onSave}
|
||||||
|
resetTraffic={resetTraffic}
|
||||||
onOpenChange={setFormOpen}
|
onOpenChange={setFormOpen}
|
||||||
/>
|
/>
|
||||||
</LazyMount>
|
</LazyMount>
|
||||||
|
|||||||
Reference in New Issue
Block a user