From 886376db7d8385304855fbde3c42501617b1d756 Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Fri, 22 May 2026 02:55:25 +0200 Subject: [PATCH] chore(frontend): antd v6 polish, theme + modal fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - adopt message.useMessage hook + messageBus bridge so HttpUtil messages inherit ConfigProvider theme tokens - replace deprecated antd APIs (List, Input addonBefore/After, Empty imageStyle); introduce InputAddon helper + SettingListItem custom rows - fix dark/ultra selectors in portaled modals (body.dark, html[data-theme='ultra-dark']) instead of nonexistent .is-dark/.is-ultra - add horizontal scroll to clients table; reorder node columns so actions+enable sit at the left - swap raw button for antd Button in NodeFormModal test connection - fix FinalMaskForm nested-form by hoisting it outside OutboundFormModal's parent Form - fix advanced "all" JSON tab in InboundFormModal — useMemo on a mutated ref was stale; compute on every render - fix chart-on-open for SystemHistory + XrayMetrics modals by adding open to effect deps (useRef.current doesn't trigger re-runs) - switch i18next interpolation to single-brace {var} to match locale files - drop residual Vue mentions in CI workflows and Go comments --- .github/workflows/ci.yml | 2 - .github/workflows/codeql.yml | 2 - frontend/src/components/AppBridge.tsx | 11 + frontend/src/components/InputAddon.css | 40 +++ frontend/src/components/InputAddon.tsx | 21 ++ frontend/src/components/SettingListItem.css | 43 ++++ frontend/src/components/SettingListItem.tsx | 12 +- frontend/src/components/TextModal.tsx | 18 +- frontend/src/hooks/useTheme.tsx | 1 - frontend/src/i18n/react.ts | 2 +- frontend/src/pages/api-docs/CodeBlock.tsx | 6 +- .../src/pages/clients/ClientBulkAddModal.tsx | 20 +- .../src/pages/clients/ClientFormModal.tsx | 18 +- .../src/pages/clients/ClientInfoModal.tsx | 20 +- frontend/src/pages/clients/ClientsPage.tsx | 21 +- .../src/pages/inbounds/InboundFormModal.tsx | 231 +++++++++++------- .../src/pages/inbounds/InboundInfoModal.tsx | 5 +- frontend/src/pages/inbounds/InboundsPage.tsx | 30 ++- frontend/src/pages/inbounds/QrCodeModal.tsx | 38 +-- frontend/src/pages/inbounds/QrPanel.tsx | 6 +- frontend/src/pages/index/BackupModal.css | 48 ++++ frontend/src/pages/index/BackupModal.tsx | 30 +-- .../src/pages/index/CustomGeoFormModal.tsx | 20 +- frontend/src/pages/index/CustomGeoSection.tsx | 6 +- frontend/src/pages/index/IndexPage.tsx | 6 +- frontend/src/pages/index/PanelUpdateModal.css | 20 ++ frontend/src/pages/index/PanelUpdateModal.tsx | 18 +- .../src/pages/index/SystemHistoryModal.tsx | 8 +- frontend/src/pages/index/VersionModal.css | 19 ++ frontend/src/pages/index/VersionModal.tsx | 18 +- frontend/src/pages/index/XrayMetricsModal.tsx | 12 +- frontend/src/pages/login/LoginPage.tsx | 8 + frontend/src/pages/nodes/NodeFormModal.tsx | 23 +- frontend/src/pages/nodes/NodeList.tsx | 63 +++-- frontend/src/pages/nodes/NodesPage.tsx | 16 +- frontend/src/pages/settings/SecurityTab.css | 6 + frontend/src/pages/settings/SecurityTab.tsx | 17 +- frontend/src/pages/settings/SettingsPage.tsx | 8 + .../pages/settings/SubscriptionFormatsTab.tsx | 17 +- .../src/pages/settings/TwoFactorModal.tsx | 20 +- frontend/src/pages/sub/SubPage.tsx | 8 +- frontend/src/pages/xray/DnsPresetsModal.css | 22 ++ frontend/src/pages/xray/DnsPresetsModal.tsx | 10 +- frontend/src/pages/xray/DnsServerModal.tsx | 51 ++-- frontend/src/pages/xray/NordModal.tsx | 16 +- frontend/src/pages/xray/OutboundFormModal.tsx | 85 ++++--- frontend/src/pages/xray/RuleFormModal.tsx | 11 +- frontend/src/pages/xray/WarpModal.tsx | 10 +- frontend/src/pages/xray/XrayPage.tsx | 6 +- frontend/src/utils/index.js | 4 +- frontend/src/utils/messageBus.ts | 12 + sub/subController.go | 3 - web/controller/xui.go | 2 +- web/web.go | 8 +- 54 files changed, 779 insertions(+), 399 deletions(-) create mode 100644 frontend/src/components/AppBridge.tsx create mode 100644 frontend/src/components/InputAddon.css create mode 100644 frontend/src/components/InputAddon.tsx create mode 100644 frontend/src/components/SettingListItem.css create mode 100644 frontend/src/utils/messageBus.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b2333fb6..9a4f9110 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,6 @@ on: - "**.mjs" - "**.cjs" - "**.ts" - - "**.vue" - "**.html" - "**.css" - "frontend/package.json" @@ -27,7 +26,6 @@ on: - "**.mjs" - "**.cjs" - "**.ts" - - "**.vue" - "**.html" - "**.css" - "frontend/package.json" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 966c581b..31f7d215 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -14,7 +14,6 @@ on: - "**.mjs" - "**.cjs" - "**.ts" - - "**.vue" - "frontend/package-lock.json" pull_request: paths: @@ -25,7 +24,6 @@ on: - "**.mjs" - "**.cjs" - "**.ts" - - "**.vue" - "frontend/package-lock.json" schedule: - cron: "18 2 * * 2" diff --git a/frontend/src/components/AppBridge.tsx b/frontend/src/components/AppBridge.tsx new file mode 100644 index 00000000..f8d05786 --- /dev/null +++ b/frontend/src/components/AppBridge.tsx @@ -0,0 +1,11 @@ +import { useEffect } from 'react'; +import { App } from 'antd'; +import { setMessageInstance } from '@/utils/messageBus'; + +export default function AppBridge({ children }: { children: React.ReactNode }) { + const { message } = App.useApp(); + useEffect(() => { + setMessageInstance(message); + }, [message]); + return <>{children}; +} diff --git a/frontend/src/components/InputAddon.css b/frontend/src/components/InputAddon.css new file mode 100644 index 00000000..e8544941 --- /dev/null +++ b/frontend/src/components/InputAddon.css @@ -0,0 +1,40 @@ +.input-addon { + display: inline-flex; + align-items: center; + padding: 0 11px; + height: 32px; + font-size: 14px; + line-height: 30px; + background-color: rgba(0, 0, 0, 0.02); + border: 1px solid #d9d9d9; + border-radius: 6px; + position: relative; + z-index: 1; + color: rgba(0, 0, 0, 0.88); + white-space: nowrap; +} + +body.dark .input-addon, +html[data-theme='ultra-dark'] .input-addon { + background-color: rgba(255, 255, 255, 0.04); + border-color: #424242; + color: rgba(255, 255, 255, 0.85); +} + +.ant-space-compact > .input-addon:not(:first-child) { + margin-inline-start: -1px; +} + +.ant-space-compact > .input-addon:first-child { + border-start-end-radius: 0; + border-end-end-radius: 0; +} + +.ant-space-compact > .input-addon:last-child { + border-start-start-radius: 0; + border-end-start-radius: 0; +} + +.ant-space-compact > .input-addon:not(:first-child):not(:last-child) { + border-radius: 0; +} diff --git a/frontend/src/components/InputAddon.tsx b/frontend/src/components/InputAddon.tsx new file mode 100644 index 00000000..b282bb53 --- /dev/null +++ b/frontend/src/components/InputAddon.tsx @@ -0,0 +1,21 @@ +import type { CSSProperties, ReactNode } from 'react'; +import './InputAddon.css'; + +interface InputAddonProps { + children: ReactNode; + className?: string; + style?: CSSProperties; + onClick?: () => void; +} + +export default function InputAddon({ children, className = '', style, onClick }: InputAddonProps) { + return ( + + {children} + + ); +} diff --git a/frontend/src/components/SettingListItem.css b/frontend/src/components/SettingListItem.css new file mode 100644 index 00000000..2f024eda --- /dev/null +++ b/frontend/src/components/SettingListItem.css @@ -0,0 +1,43 @@ +.setting-list-item { + display: flex; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid rgba(5, 5, 5, 0.06); +} + +.setting-list-item:last-child { + border-bottom: 0; +} + +body.dark .setting-list-item, +html[data-theme='ultra-dark'] .setting-list-item { + border-bottom-color: rgba(255, 255, 255, 0.08); +} + +.setting-list-meta { + display: flex; + flex-direction: column; + gap: 4px; +} + +.setting-list-title { + font-size: 14px; + color: rgba(0, 0, 0, 0.88); + font-weight: 500; +} + +.setting-list-description { + font-size: 14px; + color: rgba(0, 0, 0, 0.45); + line-height: 1.5715; +} + +body.dark .setting-list-title, +html[data-theme='ultra-dark'] .setting-list-title { + color: rgba(255, 255, 255, 0.85); +} + +body.dark .setting-list-description, +html[data-theme='ultra-dark'] .setting-list-description { + color: rgba(255, 255, 255, 0.45); +} diff --git a/frontend/src/components/SettingListItem.tsx b/frontend/src/components/SettingListItem.tsx index b9a5f116..542d80d8 100644 --- a/frontend/src/components/SettingListItem.tsx +++ b/frontend/src/components/SettingListItem.tsx @@ -1,5 +1,6 @@ import type { ReactNode } from 'react'; -import { Col, List, Row } from 'antd'; +import { Col, Row } from 'antd'; +import './SettingListItem.css'; interface SettingListItemProps { paddings?: 'small' | 'default'; @@ -18,15 +19,18 @@ export default function SettingListItem({ }: SettingListItemProps) { const padding = paddings === 'small' ? '10px 20px' : '20px'; return ( - +
- +
+ {title &&
{title}
} + {description &&
{description}
} +
{control ?? children}
- +
); } diff --git a/frontend/src/components/TextModal.tsx b/frontend/src/components/TextModal.tsx index 967e5c8e..40c96762 100644 --- a/frontend/src/components/TextModal.tsx +++ b/frontend/src/components/TextModal.tsx @@ -12,10 +12,11 @@ interface TextModalProps { } export default function TextModal({ open, onClose, title, content, fileName = '' }: TextModalProps) { + const [messageApi, messageContextHolder] = message.useMessage(); async function copy() { const ok = await ClipboardManager.copyText(content || ''); if (ok) { - message.success('Copied'); + messageApi.success('Copied'); onClose(); } } @@ -26,11 +27,13 @@ export default function TextModal({ open, onClose, title, content, fileName = '' } return ( - + {messageContextHolder} + {fileName && ( @@ -50,6 +53,7 @@ export default function TextModal({ open, onClose, title, content, fileName = '' overflowY: 'auto', }} /> - + + ); } diff --git a/frontend/src/hooks/useTheme.tsx b/frontend/src/hooks/useTheme.tsx index 8a7d6bc1..000ff943 100644 --- a/frontend/src/hooks/useTheme.tsx +++ b/frontend/src/hooks/useTheme.tsx @@ -23,7 +23,6 @@ function applyDom(isDark: boolean, isUltra: boolean) { if (msg) msg.className = isDark ? 'dark' : 'light'; } -// Mirror the Vue useTheme module: apply current localStorage state at // module load so the document is in the right theme before React mounts. const initialDark = readBool(STORAGE_DARK, true); const initialUltra = readBool(STORAGE_ULTRA, false); diff --git a/frontend/src/i18n/react.ts b/frontend/src/i18n/react.ts index cb0bd521..bf7279b5 100644 --- a/frontend/src/i18n/react.ts +++ b/frontend/src/i18n/react.ts @@ -25,7 +25,7 @@ export async function readyI18n() { lng: active, fallbackLng: FALLBACK, resources: { [FALLBACK]: { translation: enUS } }, - interpolation: { escapeValue: false }, + interpolation: { escapeValue: false, prefix: '{', suffix: '}' }, returnNull: false, }); if (active !== FALLBACK) { diff --git a/frontend/src/pages/api-docs/CodeBlock.tsx b/frontend/src/pages/api-docs/CodeBlock.tsx index b4469522..35c56f3a 100644 --- a/frontend/src/pages/api-docs/CodeBlock.tsx +++ b/frontend/src/pages/api-docs/CodeBlock.tsx @@ -29,6 +29,7 @@ function highlightJson(str: string): string { export default function CodeBlock({ code = '', lang = 'json' }: CodeBlockProps) { const [copied, setCopied] = useState(false); + const [messageApi, messageContextHolder] = message.useMessage(); const highlighted = useMemo( () => (lang === 'json' ? highlightJson(code) : escapeHtml(code)), @@ -39,15 +40,16 @@ export default function CodeBlock({ code = '', lang = 'json' }: CodeBlockProps) try { await navigator.clipboard.writeText(code); setCopied(true); - message.success('Copied'); + messageApi.success('Copied'); window.setTimeout(() => setCopied(false), 2000); } catch { - message.error('Copy failed'); + messageApi.error('Copy failed'); } } return (
+ {messageContextHolder}
{lang.toUpperCase()} {(ib.settings.gateway || []).map((_ip: string, j: number) => ( - { ib.settings.gateway[j] = e.target.value; refresh(); }} - addonAfter={} /> + + { ib.settings.gateway[j] = e.target.value; refresh(); }} /> + + ))} @@ -1252,11 +1270,15 @@ export default function InboundFormModal({ {(ib.settings.dns || []).map((_ip: string, j: number) => ( - { ib.settings.dns[j] = e.target.value; refresh(); }} - addonAfter={} /> + + { ib.settings.dns[j] = e.target.value; refresh(); }} /> + + ))} @@ -1268,11 +1290,15 @@ export default function InboundFormModal({ {(ib.settings.autoSystemRoutingTable || []).map((_ip: string, j: number) => ( - { ib.settings.autoSystemRoutingTable[j] = e.target.value; refresh(); }} - addonAfter={} /> + + { ib.settings.autoSystemRoutingTable[j] = e.target.value; refresh(); }} /> + + ))} Auto outbounds interface}> @@ -1326,12 +1352,16 @@ export default function InboundFormModal({ {(peer.allowedIPs || []).map((_ip: string, j: number) => ( - { peer.allowedIPs[j] = e.target.value; refresh(); }} - addonAfter={peer.allowedIPs.length > 1 - ? - : undefined} /> + + { peer.allowedIPs[j] = e.target.value; refresh(); }} /> + {peer.allowedIPs.length > 1 && ( + + )} + ))} @@ -1388,12 +1418,16 @@ export default function InboundFormModal({ {t('pages.inbounds.stream.tcp.path')} }> {(ib.stream.tcp.request.path || []).map((_p: string, idx: number) => ( - { ib.stream.tcp.request.path[idx] = e.target.value; refresh(); }} - addonAfter={ib.stream.tcp.request.path.length > 1 - ? - : undefined} /> + + { ib.stream.tcp.request.path[idx] = e.target.value; refresh(); }} /> + {ib.stream.tcp.request.path.length > 1 && ( + + )} + ))} @@ -1405,10 +1439,11 @@ export default function InboundFormModal({ {(ib.stream.tcp.request.headers as { name: string; value: string }[]).map((h, idx) => ( - {String(idx + 1)} + { h.name = e.target.value; refresh(); }} /> - { h.value = e.target.value; refresh(); }} />
- - +
+
+
{t('pages.index.importDatabase')}
+
{t('pages.index.importDatabaseDesc')}
+
+
); } diff --git a/frontend/src/pages/index/CustomGeoFormModal.tsx b/frontend/src/pages/index/CustomGeoFormModal.tsx index 3d0e1183..9f4b087e 100644 --- a/frontend/src/pages/index/CustomGeoFormModal.tsx +++ b/frontend/src/pages/index/CustomGeoFormModal.tsx @@ -25,6 +25,7 @@ export default function CustomGeoFormModal({ onSaved, }: CustomGeoFormModalProps) { const { t } = useTranslation(); + const [messageApi, messageContextHolder] = message.useMessage(); const [type, setType] = useState<'geosite' | 'geoip'>('geosite'); const [alias, setAlias] = useState(''); const [url, setUrl] = useState(''); @@ -47,22 +48,22 @@ export default function CustomGeoFormModal({ function validate(): boolean { if (!/^[a-z0-9_-]+$/.test(alias || '')) { - message.error(t('pages.index.customGeoValidationAlias')); + messageApi.error(t('pages.index.customGeoValidationAlias')); return false; } const u = (url || '').trim(); if (!/^https?:\/\//i.test(u)) { - message.error(t('pages.index.customGeoValidationUrl')); + messageApi.error(t('pages.index.customGeoValidationUrl')); return false; } try { const parsed = new URL(u); if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { - message.error(t('pages.index.customGeoValidationUrl')); + messageApi.error(t('pages.index.customGeoValidationUrl')); return false; } } catch { - message.error(t('pages.index.customGeoValidationUrl')); + messageApi.error(t('pages.index.customGeoValidationUrl')); return false; } return true; @@ -86,9 +87,11 @@ export default function CustomGeoFormModal({ } return ( - + {messageContextHolder} + - + + ); } diff --git a/frontend/src/pages/index/CustomGeoSection.tsx b/frontend/src/pages/index/CustomGeoSection.tsx index 6dd8d1e6..b87005a8 100644 --- a/frontend/src/pages/index/CustomGeoSection.tsx +++ b/frontend/src/pages/index/CustomGeoSection.tsx @@ -51,6 +51,7 @@ function extDisplay(record: CustomGeoListRecord): string { export default function CustomGeoSection({ active }: CustomGeoSectionProps) { const { t } = useTranslation(); const [modal, modalContextHolder] = Modal.useModal(); + const [messageApi, messageContextHolder] = message.useMessage(); const [list, setList] = useState([]); const [loading, setLoading] = useState(false); const [updatingAll, setUpdatingAll] = useState(false); @@ -85,7 +86,7 @@ export default function CustomGeoSection({ active }: CustomGeoSectionProps) { async function copyExt(record: CustomGeoListRecord) { const text = extDisplay(record); const ok = await ClipboardManager.copyText(text); - if (ok) message.success(`${t('copied')}: ${text}`); + if (ok) messageApi.success(`${t('copied')}: ${text}`); } function confirmDelete(record: CustomGeoListRecord) { @@ -120,7 +121,7 @@ export default function CustomGeoSection({ active }: CustomGeoSectionProps) { const failed = msg?.obj?.failed?.length || 0; if (msg?.success || ok > 0) { await loadList(); - if (failed > 0) message.warning(`Updated ${ok}, failed ${failed}`); + if (failed > 0) messageApi.warning(`Updated ${ok}, failed ${failed}`); } } finally { setUpdatingAll(false); @@ -229,6 +230,7 @@ export default function CustomGeoSection({ active }: CustomGeoSectionProps) { return (
+ {messageContextHolder} {modalContextHolder} { setMessageInstance(messageApi); }, [messageApi]); const [ipLimitEnable, setIpLimitEnable] = useState(false); const [panelUpdateInfo, setPanelUpdateInfo] = useState({ @@ -139,7 +142,7 @@ export default function IndexPage() { async function copyConfig() { const ok = await ClipboardManager.copyText(configText || ''); - if (ok) message.success('Copied'); + if (ok) messageApi.success('Copied'); } function downloadConfig() { @@ -150,6 +153,7 @@ export default function IndexPage() { return ( + {messageContextHolder} diff --git a/frontend/src/pages/index/PanelUpdateModal.css b/frontend/src/pages/index/PanelUpdateModal.css index 34e2a08d..4c676c74 100644 --- a/frontend/src/pages/index/PanelUpdateModal.css +++ b/frontend/src/pages/index/PanelUpdateModal.css @@ -4,11 +4,31 @@ .version-list { width: 100%; + border: 1px solid rgba(5, 5, 5, 0.06); + border-radius: 8px; + overflow: hidden; +} + +body.dark .version-list, +html[data-theme='ultra-dark'] .version-list { + border-color: rgba(255, 255, 255, 0.12); } .version-list-item { display: flex; + align-items: center; justify-content: space-between; + padding: 12px 24px; + border-bottom: 1px solid rgba(5, 5, 5, 0.06); +} + +.version-list-item:last-child { + border-bottom: 0; +} + +body.dark .version-list-item, +html[data-theme='ultra-dark'] .version-list-item { + border-bottom-color: rgba(255, 255, 255, 0.08); } .actions-row { diff --git a/frontend/src/pages/index/PanelUpdateModal.tsx b/frontend/src/pages/index/PanelUpdateModal.tsx index 47d81d1e..0afc37e6 100644 --- a/frontend/src/pages/index/PanelUpdateModal.tsx +++ b/frontend/src/pages/index/PanelUpdateModal.tsx @@ -1,5 +1,5 @@ import { useTranslation } from 'react-i18next'; -import { Alert, Button, List, Modal, Tag } from 'antd'; +import { Alert, Button, Modal, Tag } from 'antd'; import { CloudDownloadOutlined } from '@ant-design/icons'; import axios from 'axios'; @@ -84,23 +84,23 @@ export default function PanelUpdateModal({ open, info, onClose, onBusy }: PanelU /> )} - - +
+
{t('pages.index.currentPanelVersion')} v{info.currentVersion || '?'} - +
{info.updateAvailable ? ( - +
{t('pages.index.latestPanelVersion')} {info.latestVersion || '-'} - +
) : ( - +
{t('pages.index.panelUpToDate')} {t('pages.index.panelUpToDate')} - +
)} - +
+ {testResult && (
{testResult.status === 'online' ? ( @@ -291,6 +295,7 @@ export default function NodeFormModal({ )}
- + + ); } diff --git a/frontend/src/pages/nodes/NodeList.tsx b/frontend/src/pages/nodes/NodeList.tsx index 7a16f082..3cfaee99 100644 --- a/frontend/src/pages/nodes/NodeList.tsx +++ b/frontend/src/pages/nodes/NodeList.tsx @@ -118,6 +118,37 @@ export default function NodeList({ } const columns = useMemo>(() => [ + { + title: t('pages.nodes.actions'), + align: 'center', + width: 160, + render: (_value, record) => ( + + + - +
), }, diff --git a/frontend/src/pages/settings/SettingsPage.tsx b/frontend/src/pages/settings/SettingsPage.tsx index 4f958a3a..5f2ee688 100644 --- a/frontend/src/pages/settings/SettingsPage.tsx +++ b/frontend/src/pages/settings/SettingsPage.tsx @@ -14,6 +14,7 @@ import { Spin, Tabs, Tooltip, + message, } from 'antd'; import { CloudServerOutlined, @@ -24,6 +25,7 @@ import { } from '@ant-design/icons'; import { HttpUtil, PromiseUtil } from '@/utils'; +import { setMessageInstance } from '@/utils/messageBus'; import { useTheme } from '@/hooks/useTheme'; import { useMediaQuery } from '@/hooks/useMediaQuery'; import { useAllSetting } from '@/hooks/useAllSetting'; @@ -77,6 +79,11 @@ export default function SettingsPage() { const { isDark, isUltra, antdThemeConfig } = useTheme(); const { isMobile } = useMediaQuery(); const [modal, modalContextHolder] = Modal.useModal(); + const [messageApi, messageContextHolder] = message.useMessage(); + + useEffect(() => { + setMessageInstance(messageApi); + }, [messageApi]); const { allSetting, @@ -259,6 +266,7 @@ export default function SettingsPage() { return ( + {messageContextHolder} {modalContextHolder} diff --git a/frontend/src/pages/settings/SubscriptionFormatsTab.tsx b/frontend/src/pages/settings/SubscriptionFormatsTab.tsx index bd0c0fab..de6e2f42 100644 --- a/frontend/src/pages/settings/SubscriptionFormatsTab.tsx +++ b/frontend/src/pages/settings/SubscriptionFormatsTab.tsx @@ -5,7 +5,6 @@ import { Collapse, Input, InputNumber, - List, Select, Space, Switch, @@ -258,7 +257,7 @@ export default function SubscriptionFormatsTab({ allSetting, updateSetting }: Su {fragment && ( - +
- +
)} ), @@ -299,7 +298,7 @@ export default function SubscriptionFormatsTab({ allSetting, updateSetting }: Su {noisesEnabled && ( - +
({ key: String(index), label: `Noise №${index + 1}`, @@ -340,7 +339,7 @@ export default function SubscriptionFormatsTab({ allSetting, updateSetting }: Su ), }))} /> - +
)} ), @@ -354,7 +353,7 @@ export default function SubscriptionFormatsTab({ allSetting, updateSetting }: Su {muxEnabled && ( - +
- +
)} ), @@ -395,7 +394,7 @@ export default function SubscriptionFormatsTab({ allSetting, updateSetting }: Su {directEnabled && ( - +
- +
)} ), diff --git a/frontend/src/pages/settings/TwoFactorModal.tsx b/frontend/src/pages/settings/TwoFactorModal.tsx index 8ae8a35f..b686926c 100644 --- a/frontend/src/pages/settings/TwoFactorModal.tsx +++ b/frontend/src/pages/settings/TwoFactorModal.tsx @@ -28,6 +28,7 @@ export default function TwoFactorModal({ onOpenChange, }: TwoFactorModalProps) { const { t } = useTranslation(); + const [messageApi, messageContextHolder] = message.useMessage(); const [enteredCode, setEnteredCode] = useState(''); const [qrValue, setQrValue] = useState(''); const totpRef = useRef(null); @@ -68,7 +69,7 @@ export default function TwoFactorModal({ if (totpRef.current.generate() === enteredCode) { close(true); } else { - message.error(t('pages.settings.security.twoFactorModalError')); + messageApi.error(t('pages.settings.security.twoFactorModalError')); } } @@ -78,15 +79,17 @@ export default function TwoFactorModal({ async function copyToken() { const ok = await ClipboardManager.copyText(token); - if (ok) message.success(t('copied')); + if (ok) messageApi.success(t('copied')); } return ( - + {messageContextHolder} + {t('cancel')}, -
+
))} - + ); } diff --git a/frontend/src/pages/xray/DnsServerModal.tsx b/frontend/src/pages/xray/DnsServerModal.tsx index 1652fecb..9e1be244 100644 --- a/frontend/src/pages/xray/DnsServerModal.tsx +++ b/frontend/src/pages/xray/DnsServerModal.tsx @@ -1,7 +1,8 @@ import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Button, Divider, Form, Input, InputNumber, Modal, Select, Switch } from 'antd'; +import { Button, Divider, Form, Input, InputNumber, Modal, Select, Space, Switch } from 'antd'; import { PlusOutlined, MinusOutlined } from '@ant-design/icons'; +import InputAddon from '@/components/InputAddon'; export type DnsServerValue = | string @@ -190,39 +191,45 @@ export default function DnsServerModal({