QrCodeModal.tsx 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. import { useEffect, useMemo, useState } from 'react';
  2. import { useTranslation } from 'react-i18next';
  3. import { Collapse, Modal } from 'antd';
  4. import type { CollapseProps } from 'antd';
  5. import { Protocols } from '@/schemas/primitives';
  6. import {
  7. genAllLinks,
  8. genWireguardConfigs,
  9. genWireguardLinks,
  10. } from '@/lib/xray/inbound-link';
  11. import { inboundFromDb, type DbInboundLike } from '@/lib/xray/inbound-from-db';
  12. import QrPanel from './QrPanel';
  13. import type { SubSettings } from './useInbounds';
  14. interface ClientSetting {
  15. email?: string;
  16. subId?: string;
  17. [k: string]: unknown;
  18. }
  19. interface QrCodeModalProps {
  20. open: boolean;
  21. onClose: () => void;
  22. dbInbound: (DbInboundLike & { remark?: string }) | null;
  23. client?: ClientSetting | null;
  24. remarkModel?: string;
  25. nodeAddress?: string;
  26. subSettings?: SubSettings;
  27. }
  28. interface QrItem {
  29. key: string;
  30. header: string;
  31. value: string;
  32. downloadName?: string;
  33. }
  34. export default function QrCodeModal({
  35. open,
  36. onClose,
  37. dbInbound,
  38. client = null,
  39. remarkModel = '-io',
  40. nodeAddress = '',
  41. subSettings,
  42. }: QrCodeModalProps) {
  43. const { t } = useTranslation();
  44. const [links, setLinks] = useState<{ remark?: string; link: string }[]>([]);
  45. const [wireguardConfigs, setWireguardConfigs] = useState<string[]>([]);
  46. const [wireguardLinks, setWireguardLinks] = useState<string[]>([]);
  47. const [subLink, setSubLink] = useState('');
  48. const [subJsonLink, setSubJsonLink] = useState('');
  49. const [activeKey, setActiveKey] = useState<string[]>([]);
  50. useEffect(() => {
  51. if (!open || !dbInbound) return;
  52. const inbound = inboundFromDb(dbInbound);
  53. const fallbackHostname = window.location.hostname;
  54. if (inbound.protocol === Protocols.WIREGUARD) {
  55. const peerRemark = client?.email
  56. ? `${dbInbound.remark}-${client.email}`
  57. : dbInbound.remark || '';
  58. setWireguardConfigs(
  59. genWireguardConfigs({
  60. inbound,
  61. remark: peerRemark,
  62. remarkModel: '-io',
  63. hostOverride: nodeAddress,
  64. fallbackHostname,
  65. }).split('\r\n'),
  66. );
  67. setWireguardLinks(
  68. genWireguardLinks({
  69. inbound,
  70. remark: peerRemark,
  71. remarkModel: '-io',
  72. hostOverride: nodeAddress,
  73. fallbackHostname,
  74. }).split('\r\n'),
  75. );
  76. setLinks([]);
  77. } else {
  78. setLinks(
  79. genAllLinks({
  80. inbound,
  81. remark: dbInbound.remark || '',
  82. remarkModel,
  83. client: client ?? {},
  84. hostOverride: nodeAddress,
  85. fallbackHostname,
  86. }),
  87. );
  88. setWireguardConfigs([]);
  89. setWireguardLinks([]);
  90. }
  91. const subId = client?.subId;
  92. let nextSub = '';
  93. let nextSubJson = '';
  94. if (subSettings?.enable && subId) {
  95. nextSub = (subSettings.subURI || '') + subId;
  96. nextSubJson = subSettings.subJsonEnable ? (subSettings.subJsonURI || '') + subId : '';
  97. }
  98. setSubLink(nextSub);
  99. setSubJsonLink(nextSubJson);
  100. }, [open, dbInbound, client, remarkModel, nodeAddress, subSettings]);
  101. const qrItems = useMemo<QrItem[]>(() => {
  102. const items: QrItem[] = [];
  103. if (subLink) {
  104. items.push({ key: 'sub', header: t('subscription.title'), value: subLink });
  105. }
  106. if (subJsonLink) {
  107. items.push({ key: 'sub-json', header: `${t('subscription.title')} (JSON)`, value: subJsonLink });
  108. }
  109. links.forEach((link, idx) => {
  110. items.push({ key: `l${idx}`, header: link.remark || `Link ${idx + 1}`, value: link.link });
  111. });
  112. wireguardConfigs.forEach((cfg, idx) => {
  113. items.push({
  114. key: `wc${idx}`,
  115. header: `Peer ${idx + 1} config`,
  116. value: cfg,
  117. downloadName: `peer-${idx + 1}.conf`,
  118. });
  119. if (wireguardLinks[idx]) {
  120. items.push({ key: `wl${idx}`, header: `Peer ${idx + 1} link`, value: wireguardLinks[idx] });
  121. }
  122. });
  123. return items;
  124. }, [subLink, subJsonLink, links, wireguardConfigs, wireguardLinks, t]);
  125. const collapseItems: CollapseProps['items'] = useMemo(
  126. () => qrItems.map((item) => ({
  127. key: item.key,
  128. label: item.header,
  129. children: (
  130. <QrPanel
  131. value={item.value}
  132. remark={item.header}
  133. downloadName={item.downloadName || ''}
  134. showQr={!item.value.includes('mldsa65') && !item.value.includes('ML-KEM-768')}
  135. />
  136. ),
  137. })),
  138. [qrItems],
  139. );
  140. useEffect(() => {
  141. if (!open) {
  142. setActiveKey([]);
  143. return;
  144. }
  145. setActiveKey(qrItems.length > 0 ? [qrItems[0].key] : []);
  146. }, [open, qrItems]);
  147. return (
  148. <Modal open={open} onCancel={onClose} title={t('qrCode')} footer={null} width={420} destroyOnHidden>
  149. {dbInbound && collapseItems && collapseItems.length > 0 && (
  150. <Collapse
  151. ghost
  152. activeKey={activeKey}
  153. onChange={(keys) => setActiveKey(typeof keys === 'string' ? [keys] : (keys as string[]))}
  154. items={collapseItems}
  155. />
  156. )}
  157. </Modal>
  158. );
  159. }