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

feat(clients): restore reset traffic button in edit client form

MHSanaei 22 часов назад
Родитель
Сommit
4eab37b66c

+ 45 - 5
frontend/src/pages/clients/ClientFormModal.tsx

@@ -8,6 +8,7 @@ import {
   Input,
   InputNumber,
   Modal,
+  Popconfirm,
   Row,
   Select,
   Space,
@@ -17,7 +18,7 @@ import {
   Tooltip,
   message,
 } from 'antd';
-import { EyeOutlined, ReloadOutlined } from '@ant-design/icons';
+import { EyeOutlined, ReloadOutlined, RetweetOutlined } from '@ant-design/icons';
 import dayjs from 'dayjs';
 import type { Dayjs } from 'dayjs';
 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> {
   success?: boolean;
+  msg?: string;
   obj?: T;
 }
 
@@ -72,6 +74,7 @@ interface ClientFormModalProps {
     payload: Record<string, unknown> | SaveCreatePayload,
     meta: SaveMetaEdit | SaveMetaCreate,
   ) => Promise<ApiMsg | null>;
+  resetTraffic?: (client: ClientRecord) => Promise<ApiMsg | null>;
   onOpenChange: (open: boolean) => void;
 }
 
@@ -140,6 +143,7 @@ export default function ClientFormModal({
   tgBotEnable = false,
   groups = [],
   save,
+  resetTraffic,
   onOpenChange,
 }: ClientFormModalProps) {
   const { t } = useTranslation();
@@ -148,6 +152,7 @@ export default function ClientFormModal({
 
   const [form, setForm] = useState<FormState>(emptyForm);
   const [submitting, setSubmitting] = useState(false);
+  const [resetting, setResetting] = useState(false);
   const [clientIps, setClientIps] = useState<string[]>([]);
   const [ipsLoading, setIpsLoading] = useState(false);
   const [ipsClearing, setIpsClearing] = useState(false);
@@ -328,6 +333,21 @@ export default function ClientFormModal({
     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() {
     const schema = isEdit ? ClientFormSchema : ClientCreateFormSchema;
     const validated = schema.safeParse({
@@ -413,15 +433,35 @@ export default function ClientFormModal({
         open={open}
         title={isEdit ? t('pages.clients.editClient') : t('pages.clients.addClient')}
         destroyOnHidden
-        okText={isEdit ? t('save') : t('create')}
-        cancelText={t('cancel')}
-        okButtonProps={{ loading: submitting }}
         width={720}
         zIndex={CLIENT_FORM_MODAL_Z_INDEX}
         style={{ top: 20 }}
         styles={{ body: { maxHeight: 'calc(100vh - 160px)', overflowY: 'auto', overflowX: 'hidden' } }}
-        onOk={onSubmit}
         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">
           <Tabs

+ 44 - 42
frontend/src/pages/clients/ClientsPage.tsx

@@ -165,15 +165,15 @@ function gbToBytes(gb: number | undefined): number {
 }
 
 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:descend',   column: 'createdAt',  order: 'descend',  labelKey: 'pages.clients.sortNewest' },
-  { value: 'updatedAt:descend',   column: 'updatedAt',  order: 'descend',  labelKey: 'pages.clients.sortRecentlyUpdated' },
-  { value: 'lastOnline:descend',  column: 'lastOnline', order: 'descend',  labelKey: 'pages.clients.sortRecentlyOnline' },
-  { value: 'email:ascend',        column: 'email',      order: 'ascend',   labelKey: 'pages.clients.sortEmailAZ' },
-  { value: 'email:descend',       column: 'email',      order: 'descend',  labelKey: 'pages.clients.sortEmailZA' },
-  { value: 'traffic:descend',     column: 'traffic',    order: 'descend',  labelKey: 'pages.clients.sortMostTraffic' },
-  { value: 'remaining:descend',   column: 'remaining',  order: 'descend',  labelKey: 'pages.clients.sortHighestRemaining' },
-  { value: 'expiryTime:ascend',   column: 'expiryTime', order: 'ascend',   labelKey: 'pages.clients.sortExpiringSoonest' },
+  { value: 'createdAt:ascend', column: 'createdAt', order: 'ascend', labelKey: 'pages.clients.sortOldest' },
+  { value: 'createdAt:descend', column: 'createdAt', order: 'descend', labelKey: 'pages.clients.sortNewest' },
+  { value: 'updatedAt:descend', column: 'updatedAt', order: 'descend', labelKey: 'pages.clients.sortRecentlyUpdated' },
+  { value: 'lastOnline:descend', column: 'lastOnline', order: 'descend', labelKey: 'pages.clients.sortRecentlyOnline' },
+  { value: 'email:ascend', column: 'email', order: 'ascend', labelKey: 'pages.clients.sortEmailAZ' },
+  { value: 'email:descend', column: 'email', order: 'descend', labelKey: 'pages.clients.sortEmailZA' },
+  { value: 'traffic:descend', column: 'traffic', order: 'descend', labelKey: 'pages.clients.sortMostTraffic' },
+  { value: 'remaining:descend', column: 'remaining', order: 'descend', labelKey: 'pages.clients.sortHighestRemaining' },
+  { value: 'expiryTime:ascend', column: 'expiryTime', order: 'ascend', labelKey: 'pages.clients.sortExpiringSoonest' },
 ];
 
 const DEFAULT_SORT = SORT_OPTIONS[0];
@@ -743,6 +743,7 @@ export default function ClientsPage() {
     {
       title: t('pages.clients.traffic'),
       key: 'traffic',
+      width: 300,
       render: (_v, record) => (
         <ClientTrafficCell
           up={record.traffic?.up}
@@ -924,40 +925,40 @@ export default function ClientsPage() {
                             menu={{
                               items: selectedRowKeys.length > 0
                                 ? [
-                                    {
-                                      key: 'adjust',
-                                      icon: <ClockCircleOutlined />,
-                                      label: t('pages.clients.adjust'),
-                                      onClick: () => setBulkAdjustOpen(true),
-                                    },
-                                    {
-                                      key: 'subLinks',
-                                      icon: <LinkOutlined />,
-                                      label: t('pages.clients.subLinks'),
-                                      onClick: () => setSubLinksOpen(true),
-                                    },
-                                  ]
+                                  {
+                                    key: 'adjust',
+                                    icon: <ClockCircleOutlined />,
+                                    label: t('pages.clients.adjust'),
+                                    onClick: () => setBulkAdjustOpen(true),
+                                  },
+                                  {
+                                    key: 'subLinks',
+                                    icon: <LinkOutlined />,
+                                    label: t('pages.clients.subLinks'),
+                                    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,
-                                    },
-                                  ],
+                                  {
+                                    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 />}>
@@ -1246,6 +1247,7 @@ export default function ClientsPage() {
             tgBotEnable={tgBotEnable}
             groups={allGroups}
             save={onSave}
+            resetTraffic={resetTraffic}
             onOpenChange={setFormOpen}
           />
         </LazyMount>