QrCodeModal.tsx 4.8 KB

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