BulkAttachInboundsModal.tsx 3.0 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
  1. import { useEffect, useMemo, useState } from 'react';
  2. import { useTranslation } from 'react-i18next';
  3. import { Alert, Modal, Select, Typography, message } from 'antd';
  4. import type { InboundOption } from '@/hooks/useClients';
  5. import type { BulkAttachResult } from '@/schemas/client';
  6. const MULTI_USER_PROTOCOLS = new Set(['vmess', 'vless', 'trojan', 'hysteria', 'shadowsocks']);
  7. interface BulkAttachInboundsModalProps {
  8. open: boolean;
  9. count: number;
  10. inbounds: InboundOption[];
  11. onOpenChange: (open: boolean) => void;
  12. onSubmit: (inboundIds: number[]) => Promise<BulkAttachResult | null>;
  13. }
  14. export default function BulkAttachInboundsModal({
  15. open,
  16. count,
  17. inbounds,
  18. onOpenChange,
  19. onSubmit,
  20. }: BulkAttachInboundsModalProps) {
  21. const { t } = useTranslation();
  22. const [messageApi, messageContextHolder] = message.useMessage();
  23. const [targetIds, setTargetIds] = useState<number[]>([]);
  24. const [submitting, setSubmitting] = useState(false);
  25. useEffect(() => {
  26. if (open) setTargetIds([]);
  27. }, [open]);
  28. const targetOptions = useMemo(() => {
  29. return (inbounds || [])
  30. .filter((ib) => MULTI_USER_PROTOCOLS.has((ib.protocol || '').toLowerCase()))
  31. .map((ib) => ({
  32. value: ib.id,
  33. label: `${ib.remark ?? ''} (${ib.protocol ?? ''}@${ib.port ?? ''})`,
  34. }));
  35. }, [inbounds]);
  36. async function submit() {
  37. if (targetIds.length === 0 || count === 0) return;
  38. setSubmitting(true);
  39. try {
  40. const result = await onSubmit(targetIds);
  41. if (!result) return;
  42. const attached = result.attached?.length ?? 0;
  43. const skipped = result.skipped?.length ?? 0;
  44. const errors = result.errors?.length ?? 0;
  45. if (errors > 0) {
  46. messageApi.warning(
  47. t('pages.inbounds.attachClientsResultMixed', { attached, skipped, errors }),
  48. );
  49. } else {
  50. messageApi.success(t('pages.inbounds.attachClientsResult', { attached, skipped }));
  51. }
  52. onOpenChange(false);
  53. } finally {
  54. setSubmitting(false);
  55. }
  56. }
  57. return (
  58. <>
  59. {messageContextHolder}
  60. <Modal
  61. open={open}
  62. title={t('pages.clients.attachToInboundsTitle', { count })}
  63. okText={t('pages.inbounds.attachClients')}
  64. cancelText={t('cancel')}
  65. okButtonProps={{ disabled: targetIds.length === 0, loading: submitting }}
  66. onCancel={() => onOpenChange(false)}
  67. onOk={submit}
  68. destroyOnHidden
  69. >
  70. <Typography.Paragraph type="secondary">
  71. {t('pages.clients.attachToInboundsDesc', { count })}
  72. </Typography.Paragraph>
  73. {targetOptions.length === 0 ? (
  74. <Alert type="info" showIcon message={t('pages.clients.attachToInboundsNoTargets')} />
  75. ) : (
  76. <Select
  77. mode="multiple"
  78. style={{ width: '100%' }}
  79. value={targetIds}
  80. onChange={setTargetIds}
  81. options={targetOptions}
  82. placeholder={t('pages.clients.attachToInboundsTargets')}
  83. optionFilterProp="label"
  84. autoFocus
  85. />
  86. )}
  87. </Modal>
  88. </>
  89. );
  90. }