InboundsPage.tsx 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596
  1. import { lazy, useCallback, useEffect, useMemo, useState } from 'react';
  2. import { useTranslation } from 'react-i18next';
  3. import {
  4. Card,
  5. Col,
  6. ConfigProvider,
  7. Layout,
  8. Modal,
  9. Row,
  10. Spin,
  11. Statistic,
  12. message,
  13. } from 'antd';
  14. import { setMessageInstance } from '@/utils/messageBus';
  15. import {
  16. SwapOutlined,
  17. PieChartOutlined,
  18. BarsOutlined,
  19. } from '@ant-design/icons';
  20. import { HttpUtil, SizeFormatter, RandomUtil } from '@/utils';
  21. import { createDefaultInboundSettings } from '@/lib/xray/inbound-defaults';
  22. import { genInboundLinks } from '@/lib/xray/inbound-link';
  23. import { inboundFromDb } from '@/lib/xray/inbound-from-db';
  24. import { coerceInboundJsonField, type DBInbound } from '@/models/dbinbound';
  25. import { useTheme } from '@/hooks/useTheme';
  26. import { useMediaQuery } from '@/hooks/useMediaQuery';
  27. import { useWebSocket } from '@/hooks/useWebSocket';
  28. import { useNodesQuery } from '@/api/queries/useNodesQuery';
  29. import AppSidebar from '@/components/AppSidebar';
  30. const TextModal = lazy(() => import('@/components/TextModal'));
  31. const PromptModal = lazy(() => import('@/components/PromptModal'));
  32. import { useInbounds } from './useInbounds';
  33. import InboundList from './InboundList';
  34. import LazyMount from '@/components/LazyMount';
  35. const InboundFormModal = lazy(() => import('./InboundFormModal'));
  36. const InboundInfoModal = lazy(() => import('./InboundInfoModal'));
  37. const QrCodeModal = lazy(() => import('./QrCodeModal'));
  38. type RowAction =
  39. | 'edit'
  40. | 'showInfo'
  41. | 'qrcode'
  42. | 'export'
  43. | 'subs'
  44. | 'clipboard'
  45. | 'delete'
  46. | 'resetTraffic'
  47. | 'clone';
  48. type GeneralAction = 'import' | 'export' | 'subs' | 'resetInbounds';
  49. interface ClientMatchTarget {
  50. id?: string;
  51. email?: string;
  52. password?: string;
  53. }
  54. export default function InboundsPage() {
  55. const { t } = useTranslation();
  56. const { isDark, isUltra, antdThemeConfig } = useTheme();
  57. const { isMobile } = useMediaQuery();
  58. const {
  59. fetched,
  60. dbInbounds,
  61. clientCount,
  62. onlineClients,
  63. lastOnlineMap,
  64. totals,
  65. expireDiff,
  66. trafficDiff,
  67. pageSize,
  68. subSettings,
  69. tgBotEnable,
  70. ipLimitEnable,
  71. remarkModel,
  72. refresh,
  73. hydrateInbound,
  74. applyTrafficEvent,
  75. applyClientStatsEvent,
  76. } = useInbounds();
  77. const [modal, modalContextHolder] = Modal.useModal();
  78. const [messageApi, messageContextHolder] = message.useMessage();
  79. useEffect(() => { setMessageInstance(messageApi); }, [messageApi]);
  80. const { nodes: nodesList } = useNodesQuery();
  81. const nodesById = useMemo(() => {
  82. const map = new Map<number, ReturnType<typeof useNodesQuery>['nodes'][number]>();
  83. for (const n of nodesList || []) map.set(n.id, n);
  84. return map;
  85. }, [nodesList]);
  86. const hasActiveNode = useMemo(
  87. () => (nodesList || []).some((n) => n.enable && n.status === 'online'),
  88. [nodesList],
  89. );
  90. const hasNodeAttachedInbound = useMemo(
  91. () => (dbInbounds || []).some((ib) => ib?.nodeId != null),
  92. [dbInbounds],
  93. );
  94. const showNodeInfo = hasNodeAttachedInbound || hasActiveNode;
  95. useWebSocket({
  96. traffic: applyTrafficEvent,
  97. client_stats: applyClientStatsEvent,
  98. });
  99. const [formOpen, setFormOpen] = useState(false);
  100. const [formMode, setFormMode] = useState<'add' | 'edit'>('add');
  101. const [formDbInbound, setFormDbInbound] = useState<DBInbound | null>(null);
  102. const [infoOpen, setInfoOpen] = useState(false);
  103. const [infoDbInbound, setInfoDbInbound] = useState<DBInbound | null>(null);
  104. const [infoClientIndex, setInfoClientIndex] = useState(0);
  105. const [qrOpen, setQrOpen] = useState(false);
  106. const [qrDbInbound, setQrDbInbound] = useState<DBInbound | null>(null);
  107. const [textOpen, setTextOpen] = useState(false);
  108. const [textTitle, setTextTitle] = useState('');
  109. const [textContent, setTextContent] = useState('');
  110. const [textFileName, setTextFileName] = useState('');
  111. const [promptOpen, setPromptOpen] = useState(false);
  112. const [promptTitle, setPromptTitle] = useState('');
  113. const [promptOkText, setPromptOkText] = useState('OK');
  114. const [promptType, setPromptType] = useState<'textarea' | 'input'>('textarea');
  115. const [promptInitial, setPromptInitial] = useState('');
  116. const [promptLoading, setPromptLoading] = useState(false);
  117. const [promptHandler, setPromptHandler] = useState<((value: string) => Promise<boolean | void> | boolean | void) | null>(null);
  118. const hostOverrideFor = useCallback((dbInbound: DBInbound | null) => {
  119. if (!dbInbound || dbInbound.nodeId == null) return '';
  120. return nodesById.get(dbInbound.nodeId)?.address || '';
  121. }, [nodesById]);
  122. const infoNodeAddress = useMemo(() => hostOverrideFor(infoDbInbound), [infoDbInbound, hostOverrideFor]);
  123. const qrNodeAddress = useMemo(() => hostOverrideFor(qrDbInbound), [qrDbInbound, hostOverrideFor]);
  124. const openText = useCallback((opts: { title: string; content: string; fileName?: string }) => {
  125. setTextTitle(opts.title);
  126. setTextContent(opts.content);
  127. setTextFileName(opts.fileName || '');
  128. setTextOpen(true);
  129. }, []);
  130. const openPrompt = useCallback((opts: {
  131. title: string;
  132. okText?: string;
  133. type?: 'textarea' | 'input';
  134. value?: string;
  135. confirm: (value: string) => Promise<boolean | void> | boolean | void;
  136. }) => {
  137. setPromptTitle(opts.title);
  138. setPromptOkText(opts.okText || 'OK');
  139. setPromptType(opts.type || 'textarea');
  140. setPromptInitial(opts.value || '');
  141. setPromptHandler(() => opts.confirm);
  142. setPromptOpen(true);
  143. }, []);
  144. const onPromptConfirm = useCallback(async (value: string) => {
  145. if (!promptHandler) {
  146. setPromptOpen(false);
  147. return;
  148. }
  149. setPromptLoading(true);
  150. try {
  151. const ok = await promptHandler(value);
  152. if (ok !== false) setPromptOpen(false);
  153. } finally {
  154. setPromptLoading(false);
  155. }
  156. }, [promptHandler]);
  157. const projectChildThroughMaster = useCallback((child: DBInbound, master: DBInbound): DBInbound => {
  158. const projected = JSON.parse(JSON.stringify(child)) as DBInbound;
  159. projected.listen = master.listen;
  160. projected.port = master.port;
  161. const masterStream = coerceInboundJsonField(master.streamSettings) as Record<string, unknown>;
  162. const childStream = { ...(coerceInboundJsonField(child.streamSettings) as Record<string, unknown>) };
  163. childStream.security = masterStream.security;
  164. childStream.tlsSettings = masterStream.tlsSettings;
  165. childStream.realitySettings = masterStream.realitySettings;
  166. childStream.externalProxy = masterStream.externalProxy;
  167. projected.streamSettings = JSON.stringify(childStream);
  168. const Ctor = child.constructor as new (data: DBInbound) => DBInbound;
  169. return new Ctor(projected);
  170. }, []);
  171. const checkFallback = useCallback((dbInbound: DBInbound): DBInbound => {
  172. const parent = dbInbound?.fallbackParent;
  173. if (parent?.masterId) {
  174. const master = dbInbounds.find((ib) => ib.id === parent.masterId);
  175. if (master) return projectChildThroughMaster(dbInbound, master);
  176. }
  177. if (!dbInbound?.listen?.startsWith?.('@')) return dbInbound;
  178. for (const candidate of dbInbounds) {
  179. if (candidate.id === dbInbound.id) continue;
  180. if (!['trojan', 'vless'].includes(candidate.protocol)) continue;
  181. const candStream = coerceInboundJsonField(candidate.streamSettings) as { network?: string };
  182. if (candStream.network !== 'tcp') continue;
  183. const candSettings = coerceInboundJsonField(candidate.settings) as { fallbacks?: { dest?: string }[] };
  184. const fallbacks = candSettings.fallbacks || [];
  185. if (!fallbacks.find((f) => f.dest === dbInbound.listen)) continue;
  186. return projectChildThroughMaster(dbInbound, candidate);
  187. }
  188. return dbInbound;
  189. }, [dbInbounds, projectChildThroughMaster]);
  190. const findClientIndex = useCallback((dbInbound: DBInbound, client: ClientMatchTarget | null) => {
  191. if (!client) return 0;
  192. const settings = coerceInboundJsonField(dbInbound.settings) as { clients?: ClientMatchTarget[] };
  193. const clients = settings.clients || [];
  194. const idx = clients.findIndex((c) => {
  195. if (!c) return false;
  196. switch (dbInbound.protocol) {
  197. case 'trojan':
  198. case 'shadowsocks':
  199. return c.password === client.password && c.email === client.email;
  200. default:
  201. return c.id === client.id && c.email === client.email;
  202. }
  203. });
  204. return idx >= 0 ? idx : 0;
  205. }, []);
  206. const exportInboundLinks = useCallback((dbInbound: DBInbound) => {
  207. const projected = checkFallback(dbInbound);
  208. openText({
  209. title: t('pages.inbounds.exportLinksTitle'),
  210. content: genInboundLinks({
  211. inbound: inboundFromDb(projected),
  212. remark: projected.remark,
  213. remarkModel,
  214. hostOverride: hostOverrideFor(dbInbound),
  215. fallbackHostname: window.location.hostname,
  216. }),
  217. fileName: projected.remark || 'inbound',
  218. });
  219. }, [checkFallback, remarkModel, hostOverrideFor, openText, t]);
  220. const exportInboundClipboard = useCallback((dbInbound: DBInbound) => {
  221. openText({ title: t('pages.inbounds.inboundJsonTitle'), content: JSON.stringify(dbInbound, null, 2) });
  222. }, [openText, t]);
  223. const exportInboundSubs = useCallback((dbInbound: DBInbound) => {
  224. const settings = coerceInboundJsonField(dbInbound.settings) as { clients?: { subId?: string }[] };
  225. const clients = settings.clients || [];
  226. const subLinks: string[] = [];
  227. for (const c of clients) {
  228. if (c.subId && subSettings.subURI) {
  229. subLinks.push(subSettings.subURI + c.subId);
  230. }
  231. }
  232. openText({
  233. title: t('pages.inbounds.exportSubsTitle'),
  234. content: [...new Set(subLinks)].join('\n'),
  235. fileName: `${dbInbound.remark || 'inbound'}-Subs`,
  236. });
  237. }, [subSettings, openText, t]);
  238. const exportAllLinks = useCallback(async () => {
  239. const hydrated = await Promise.all(
  240. dbInbounds.map((ib) => hydrateInbound(ib.id).then((r) => r ?? ib)),
  241. );
  242. const out: string[] = [];
  243. for (const ib of hydrated) {
  244. const projected = checkFallback(ib);
  245. out.push(genInboundLinks({
  246. inbound: inboundFromDb(projected),
  247. remark: projected.remark,
  248. remarkModel,
  249. hostOverride: hostOverrideFor(ib),
  250. fallbackHostname: window.location.hostname,
  251. }));
  252. }
  253. openText({ title: t('pages.inbounds.exportAllLinksTitle'), content: out.join('\r\n'), fileName: 'All-Inbounds' });
  254. }, [dbInbounds, hydrateInbound, checkFallback, remarkModel, hostOverrideFor, openText, t]);
  255. const exportAllSubs = useCallback(async () => {
  256. const hydrated = await Promise.all(
  257. dbInbounds.map((ib) => hydrateInbound(ib.id).then((r) => r ?? ib)),
  258. );
  259. const out: string[] = [];
  260. for (const ib of hydrated) {
  261. const settings = coerceInboundJsonField(ib.settings) as { clients?: { subId?: string }[] };
  262. const clients = settings.clients || [];
  263. for (const c of clients) {
  264. if (c.subId && subSettings.subURI) {
  265. out.push(subSettings.subURI + c.subId);
  266. }
  267. }
  268. }
  269. openText({ title: t('pages.inbounds.exportAllSubsTitle'), content: [...new Set(out)].join('\r\n'), fileName: 'All-Inbounds-Subs' });
  270. }, [dbInbounds, hydrateInbound, subSettings, openText, t]);
  271. const importInbound = useCallback(() => {
  272. openPrompt({
  273. title: 'Import inbound',
  274. okText: 'Import',
  275. type: 'textarea',
  276. value: '',
  277. confirm: async (value) => {
  278. const msg = await HttpUtil.post('/panel/api/inbounds/import', { data: value });
  279. if (msg?.success) {
  280. await refresh();
  281. return true;
  282. }
  283. return false;
  284. },
  285. });
  286. }, [openPrompt, refresh]);
  287. const onAddInbound = useCallback(() => {
  288. setFormMode('add');
  289. setFormDbInbound(null);
  290. setFormOpen(true);
  291. }, []);
  292. const openEdit = useCallback((dbInbound: DBInbound) => {
  293. setFormMode('edit');
  294. setFormDbInbound(dbInbound);
  295. setFormOpen(true);
  296. }, []);
  297. const confirmDelete = useCallback((dbInbound: DBInbound) => {
  298. modal.confirm({
  299. title: t('pages.inbounds.deleteConfirmTitle', { remark: dbInbound.remark }),
  300. content: t('pages.inbounds.deleteConfirmContent'),
  301. okText: t('delete'),
  302. okType: 'danger',
  303. cancelText: t('cancel'),
  304. onOk: async () => {
  305. const msg = await HttpUtil.post(`/panel/api/inbounds/del/${dbInbound.id}`);
  306. if (msg?.success) await refresh();
  307. },
  308. });
  309. }, [modal, refresh, t]);
  310. const confirmResetTraffic = useCallback((dbInbound: DBInbound) => {
  311. modal.confirm({
  312. title: t('pages.inbounds.resetConfirmTitle', { remark: dbInbound.remark }),
  313. content: t('pages.inbounds.resetConfirmContent'),
  314. okText: t('reset'),
  315. cancelText: t('cancel'),
  316. onOk: async () => {
  317. const msg = await HttpUtil.post(`/panel/api/inbounds/${dbInbound.id}/resetTraffic`);
  318. if (msg?.success) await refresh();
  319. },
  320. });
  321. }, [modal, refresh, t]);
  322. const confirmClone = useCallback((dbInbound: DBInbound) => {
  323. modal.confirm({
  324. title: t('pages.inbounds.cloneConfirmTitle', { remark: dbInbound.remark }),
  325. content: t('pages.inbounds.cloneConfirmContent'),
  326. okText: t('pages.inbounds.clone'),
  327. cancelText: t('cancel'),
  328. onOk: async () => {
  329. let clonedSettings: string;
  330. try {
  331. const raw = coerceInboundJsonField(dbInbound.settings);
  332. raw.clients = [];
  333. clonedSettings = JSON.stringify(raw);
  334. } catch {
  335. const fallback = createDefaultInboundSettings(dbInbound.protocol);
  336. clonedSettings = fallback ? JSON.stringify(fallback, null, 2) : '{}';
  337. }
  338. const streamSettingsString = typeof dbInbound.streamSettings === 'string'
  339. ? dbInbound.streamSettings
  340. : JSON.stringify(dbInbound.streamSettings ?? {});
  341. const sniffingString = typeof dbInbound.sniffing === 'string'
  342. ? dbInbound.sniffing
  343. : JSON.stringify(dbInbound.sniffing ?? {});
  344. const data = {
  345. up: 0,
  346. down: 0,
  347. total: 0,
  348. remark: `${dbInbound.remark} (clone)`,
  349. enable: false,
  350. expiryTime: 0,
  351. listen: '',
  352. port: RandomUtil.randomInteger(10000, 60000),
  353. protocol: dbInbound.protocol,
  354. settings: clonedSettings,
  355. streamSettings: streamSettingsString,
  356. sniffing: sniffingString,
  357. };
  358. const msg = await HttpUtil.post('/panel/api/inbounds/add', data);
  359. if (msg?.success) await refresh();
  360. },
  361. });
  362. }, [modal, refresh, t]);
  363. const onGeneralAction = useCallback((key: GeneralAction) => {
  364. switch (key) {
  365. case 'import': importInbound(); break;
  366. case 'export': exportAllLinks(); break;
  367. case 'subs': exportAllSubs(); break;
  368. case 'resetInbounds':
  369. modal.confirm({
  370. title: 'Reset all inbound traffic?',
  371. okText: 'Reset',
  372. cancelText: 'Cancel',
  373. onOk: async () => {
  374. const msg = await HttpUtil.post('/panel/api/inbounds/resetAllTraffics');
  375. if (msg?.success) await refresh();
  376. },
  377. });
  378. break;
  379. default:
  380. messageApi.info(`General action "${key}" — coming in a later 5f subphase`);
  381. }
  382. }, [modal, importInbound, exportAllLinks, exportAllSubs, refresh, messageApi]);
  383. const onRowAction = useCallback(async ({ key, dbInbound }: { key: RowAction; dbInbound: DBInbound }) => {
  384. // Actions that touch per-client secrets (uuid, password, flow, ...) need
  385. // the full payload that the slim list view does not ship. Hydrate first
  386. // and then operate on the rehydrated record.
  387. const hydratingKeys: RowAction[] = ['edit', 'showInfo', 'qrcode', 'export', 'subs', 'clipboard', 'clone'];
  388. let target = dbInbound;
  389. if (hydratingKeys.includes(key)) {
  390. const hydrated = await hydrateInbound(dbInbound.id);
  391. if (hydrated) target = hydrated;
  392. }
  393. switch (key) {
  394. case 'edit':
  395. openEdit(target);
  396. break;
  397. case 'showInfo':
  398. setInfoDbInbound(checkFallback(target));
  399. setInfoClientIndex(findClientIndex(target, null));
  400. setInfoOpen(true);
  401. break;
  402. case 'qrcode':
  403. setQrDbInbound(checkFallback(target));
  404. setQrOpen(true);
  405. break;
  406. case 'export':
  407. exportInboundLinks(target);
  408. break;
  409. case 'subs':
  410. exportInboundSubs(target);
  411. break;
  412. case 'clipboard':
  413. exportInboundClipboard(target);
  414. break;
  415. case 'delete':
  416. confirmDelete(target);
  417. break;
  418. case 'resetTraffic':
  419. confirmResetTraffic(target);
  420. break;
  421. case 'clone':
  422. confirmClone(target);
  423. break;
  424. default:
  425. messageApi.info(`Action "${key}" — coming in a later 5f subphase`);
  426. }
  427. }, [hydrateInbound, openEdit, checkFallback, findClientIndex, exportInboundLinks, exportInboundSubs, exportInboundClipboard, confirmDelete, confirmResetTraffic, confirmClone, messageApi]);
  428. return (
  429. <ConfigProvider theme={antdThemeConfig}>
  430. {messageContextHolder}
  431. {modalContextHolder}
  432. <Layout className={`inbounds-page${isDark ? ' is-dark' : ''}${isUltra ? ' is-ultra' : ''}`}>
  433. <AppSidebar />
  434. <Layout className="content-shell">
  435. <Layout.Content id="content-layout" className="content-area">
  436. <Spin spinning={!fetched} delay={200} description="Loading…" size="large">
  437. {!fetched ? (
  438. <div className="loading-spacer" />
  439. ) : (
  440. <Row gutter={[isMobile ? 8 : 16, 12]}>
  441. <Col span={24}>
  442. <Card size="small" hoverable className="summary-card">
  443. <Row gutter={[16, 12]}>
  444. <Col xs={12} sm={12} md={8}>
  445. <Statistic
  446. title={t('pages.inbounds.totalDownUp')}
  447. value={`${SizeFormatter.sizeFormat(totals.up)} / ${SizeFormatter.sizeFormat(totals.down)}`}
  448. prefix={<SwapOutlined />}
  449. />
  450. </Col>
  451. <Col xs={12} sm={12} md={8}>
  452. <Statistic
  453. title={t('pages.inbounds.totalUsage')}
  454. value={SizeFormatter.sizeFormat(totals.up + totals.down)}
  455. prefix={<PieChartOutlined />}
  456. />
  457. </Col>
  458. <Col xs={24} sm={24} md={8}>
  459. <Statistic
  460. title={t('pages.inbounds.inboundCount')}
  461. value={String(dbInbounds.length)}
  462. prefix={<BarsOutlined />}
  463. />
  464. </Col>
  465. </Row>
  466. </Card>
  467. </Col>
  468. <Col span={24}>
  469. <InboundList
  470. dbInbounds={dbInbounds}
  471. clientCount={clientCount}
  472. onlineClients={onlineClients}
  473. lastOnlineMap={lastOnlineMap}
  474. expireDiff={expireDiff}
  475. trafficDiff={trafficDiff}
  476. pageSize={pageSize}
  477. isMobile={isMobile}
  478. subEnable={subSettings.enable}
  479. nodesById={nodesById}
  480. hasActiveNode={showNodeInfo}
  481. onAddInbound={onAddInbound}
  482. onGeneralAction={onGeneralAction}
  483. onRowAction={({ key, dbInbound }) => onRowAction({ key, dbInbound: dbInbound as unknown as DBInbound })}
  484. />
  485. </Col>
  486. </Row>
  487. )}
  488. </Spin>
  489. </Layout.Content>
  490. </Layout>
  491. <LazyMount when={formOpen}>
  492. <InboundFormModal
  493. open={formOpen}
  494. onClose={() => setFormOpen(false)}
  495. onSaved={refresh}
  496. mode={formMode}
  497. dbInbound={formDbInbound}
  498. dbInbounds={dbInbounds}
  499. availableNodes={nodesList}
  500. />
  501. </LazyMount>
  502. <LazyMount when={infoOpen}>
  503. <InboundInfoModal
  504. open={infoOpen}
  505. onClose={() => setInfoOpen(false)}
  506. dbInbound={infoDbInbound}
  507. clientIndex={infoClientIndex}
  508. remarkModel={remarkModel}
  509. expireDiff={expireDiff}
  510. trafficDiff={trafficDiff}
  511. ipLimitEnable={ipLimitEnable}
  512. tgBotEnable={tgBotEnable}
  513. subSettings={subSettings}
  514. lastOnlineMap={lastOnlineMap}
  515. nodeAddress={infoNodeAddress}
  516. />
  517. </LazyMount>
  518. <LazyMount when={qrOpen}>
  519. <QrCodeModal
  520. open={qrOpen}
  521. onClose={() => setQrOpen(false)}
  522. dbInbound={qrDbInbound}
  523. client={null}
  524. remarkModel={remarkModel}
  525. nodeAddress={qrNodeAddress}
  526. subSettings={subSettings}
  527. />
  528. </LazyMount>
  529. <LazyMount when={textOpen}>
  530. <TextModal
  531. open={textOpen}
  532. onClose={() => setTextOpen(false)}
  533. title={textTitle}
  534. content={textContent}
  535. fileName={textFileName}
  536. />
  537. </LazyMount>
  538. <LazyMount when={promptOpen}>
  539. <PromptModal
  540. open={promptOpen}
  541. onClose={() => setPromptOpen(false)}
  542. title={promptTitle}
  543. okText={promptOkText}
  544. type={promptType}
  545. initialValue={promptInitial}
  546. loading={promptLoading}
  547. onConfirm={onPromptConfirm}
  548. />
  549. </LazyMount>
  550. </Layout>
  551. </ConfigProvider>
  552. );
  553. }