AttachClientsModal.tsx 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
  1. import { useEffect, useMemo, useState } from 'react';
  2. import { useTranslation } from 'react-i18next';
  3. import { Alert, Modal, Select, Typography, message } from 'antd';
  4. import { HttpUtil } from '@/utils';
  5. import { coerceInboundJsonField, type DBInbound } from '@/models/dbinbound';
  6. import { isInboundMultiUser } from './InboundList';
  7. interface AttachClientsModalProps {
  8. open: boolean;
  9. source: DBInbound | null;
  10. dbInbounds: DBInbound[];
  11. onClose: () => void;
  12. onAttached?: () => void;
  13. }
  14. interface BulkAttachResult {
  15. attached?: string[];
  16. skipped?: string[];
  17. errors?: string[];
  18. }
  19. function readClientEmails(settings: unknown): string[] {
  20. const parsed = coerceInboundJsonField(settings) as { clients?: Array<{ email?: string }> };
  21. const clients = Array.isArray(parsed?.clients) ? parsed.clients : [];
  22. return clients.map((c) => (c?.email || '').trim()).filter(Boolean);
  23. }
  24. export default function AttachClientsModal({
  25. open,
  26. source,
  27. dbInbounds,
  28. onClose,
  29. onAttached,
  30. }: AttachClientsModalProps) {
  31. const { t } = useTranslation();
  32. const [messageApi, messageContextHolder] = message.useMessage();
  33. const [targetIds, setTargetIds] = useState<number[]>([]);
  34. const [saving, setSaving] = useState(false);
  35. useEffect(() => {
  36. if (open) setTargetIds([]);
  37. }, [open]);
  38. const emails = useMemo(() => (source ? readClientEmails(source.settings) : []), [source]);
  39. const targetOptions = useMemo(() => {
  40. if (!source) return [];
  41. return (dbInbounds || [])
  42. .filter((ib) => ib.id !== source.id && isInboundMultiUser(ib))
  43. .map((ib) => ({ value: ib.id, label: `${ib.remark} (${ib.protocol}@${ib.port})` }));
  44. }, [dbInbounds, source]);
  45. async function submit() {
  46. if (!source || targetIds.length === 0 || emails.length === 0) return;
  47. setSaving(true);
  48. try {
  49. const msg = await HttpUtil.post('/panel/api/clients/bulkAttach', { emails, inboundIds: targetIds }, { headers: { 'Content-Type': 'application/json' } });
  50. if (!msg?.success) {
  51. messageApi.error(msg?.msg || t('somethingWentWrong'));
  52. return;
  53. }
  54. const result = (msg.obj || {}) as BulkAttachResult;
  55. const attached = result.attached?.length ?? 0;
  56. const skipped = result.skipped?.length ?? 0;
  57. const errors = result.errors?.length ?? 0;
  58. if (errors > 0) {
  59. messageApi.warning(t('pages.inbounds.attachClientsResultMixed', { attached, skipped, errors }));
  60. } else {
  61. messageApi.success(t('pages.inbounds.attachClientsResult', { attached, skipped }));
  62. }
  63. onAttached?.();
  64. onClose();
  65. } finally {
  66. setSaving(false);
  67. }
  68. }
  69. return (
  70. <Modal
  71. open={open}
  72. onCancel={onClose}
  73. onOk={submit}
  74. okButtonProps={{ disabled: targetIds.length === 0 || emails.length === 0, loading: saving }}
  75. okText={t('pages.inbounds.attachClients')}
  76. cancelText={t('cancel')}
  77. title={t('pages.inbounds.attachClientsTitle', { remark: source?.remark ?? '' })}
  78. >
  79. {messageContextHolder}
  80. <Typography.Paragraph type="secondary">
  81. {t('pages.inbounds.attachClientsDesc', { count: emails.length })}
  82. </Typography.Paragraph>
  83. {targetOptions.length === 0 ? (
  84. <Alert type="info" showIcon message={t('pages.inbounds.attachClientsNoTargets')} />
  85. ) : (
  86. <Select
  87. mode="multiple"
  88. style={{ width: '100%' }}
  89. value={targetIds}
  90. onChange={setTargetIds}
  91. options={targetOptions}
  92. placeholder={t('pages.inbounds.attachClientsTargets')}
  93. optionFilterProp="label"
  94. />
  95. )}
  96. </Modal>
  97. );
  98. }