0 ? dayjs(displayedExpiry) : null}
onChange={(v) => expiryField.onChange(v ? v.valueOf() : 0)}
placeholder={t('pages.inbounds.leaveBlankToNeverExpire')}
@@ -1515,7 +1572,12 @@ export default function ClientFormModal({
zIndex={CLIENT_IP_LOG_MODAL_Z_INDEX}
onCancel={() => setIpsModalOpen(false)}
footer={[
- } loading={ipsLoading} onClick={loadIps}>
+ }
+ loading={ipsLoading}
+ onClick={loadIps}
+ >
{t('refresh')}
,
}
+ icon={}
aria-label={t('copy')}
onClick={() => copyValue(client.subId!)}
/>
@@ -365,7 +365,7 @@ export default function ClientInfoModal({
}
+ icon={}
aria-label={t('copy')}
onClick={() => copyValue(client.uuid!)}
/>
@@ -380,7 +380,7 @@ export default function ClientInfoModal({
}
+ icon={}
aria-label={t('copy')}
onClick={() => copyValue(client.password!)}
/>
@@ -395,7 +395,7 @@ export default function ClientInfoModal({
}
+ icon={}
aria-label={t('copy')}
onClick={() => copyValue(client.auth!)}
/>
@@ -457,7 +457,7 @@ export default function ClientInfoModal({
|
}
+ icon={}
aria-label={t('pages.clients.ipLog')}
loading={ipsLoading}
onClick={openIpsModal}
@@ -489,7 +489,7 @@ export default function ClientInfoModal({
|
}
+ icon={}
aria-label={t('pages.clients.hwidLog')}
loading={hwidsLoading}
onClick={openHwidsModal}
@@ -591,7 +591,7 @@ export default function ClientInfoModal({
}
+ icon={}
aria-label={t('copy')}
onClick={() => copyValue(subLink)}
/>
@@ -599,7 +599,7 @@ export default function ClientInfoModal({
}
+ icon={}
aria-label={t('download')}
loading={downloadingFormat === 'standard'}
disabled={downloadingFormat !== null}
@@ -621,7 +621,7 @@ export default function ClientInfoModal({
}
+ icon={}
aria-label={t('pages.clients.qrCode')}
/>
@@ -646,7 +646,7 @@ export default function ClientInfoModal({
}
+ icon={}
aria-label={t('copy')}
onClick={() => copyValue(subJsonLink)}
/>
@@ -654,7 +654,7 @@ export default function ClientInfoModal({
}
+ icon={}
aria-label={t('download')}
loading={downloadingFormat === 'json'}
disabled={downloadingFormat !== null}
@@ -676,7 +676,7 @@ export default function ClientInfoModal({
}
+ icon={}
aria-label={t('pages.clients.qrCode')}
/>
@@ -704,7 +704,7 @@ export default function ClientInfoModal({
}
+ icon={}
aria-label={t('copy')}
onClick={() => copyValue(subClashLink)}
/>
@@ -712,7 +712,7 @@ export default function ClientInfoModal({
}
+ icon={}
aria-label={t('download')}
loading={downloadingFormat === 'clash'}
disabled={downloadingFormat !== null}
@@ -734,7 +734,7 @@ export default function ClientInfoModal({
}
+ icon={}
aria-label={t('pages.clients.qrCode')}
/>
@@ -768,7 +768,7 @@ export default function ClientInfoModal({
}
+ icon={}
aria-label={t('copy')}
onClick={() => copyValue(link)}
/>
@@ -783,7 +783,7 @@ export default function ClientInfoModal({
}
+ icon={}
aria-label={t('pages.clients.qrCode')}
/>
@@ -853,7 +853,12 @@ export default function ClientInfoModal({
width={440}
onCancel={() => setIpsModalOpen(false)}
footer={[
- } loading={ipsLoading} onClick={loadIps}>
+ }
+ loading={ipsLoading}
+ onClick={loadIps}
+ >
{t('refresh')}
,
{selectedRowKeys.length > 0 && (
}
+ icon={}
onClick={onBulkDelete}
style={{ marginInlineStart: 'auto' }}
aria-label={t('delete')}
@@ -1536,14 +1541,14 @@ export default function ClientsPage() {
onChange={(e) => setSearchKey(e.target.value)}
placeholder={t('pages.clients.searchPlaceholder')}
allowClear
- prefix={}
+ prefix={}
size={isMobile ? 'small' : 'middle'}
style={{ maxWidth: 320 }}
aria-label={t('search')}
/>
}
+ icon={}
size={isMobile ? 'small' : 'middle'}
onClick={() => setFilterDrawerOpen(true)}
type={activeCount > 0 ? 'primary' : 'default'}
@@ -1556,7 +1561,7 @@ export default function ClientsPage() {
value={sortValueFor(sortColumn, sortOrder)}
aria-label={t('sort')}
size={isMobile ? 'small' : 'middle'}
- suffix={}
+ suffix={}
style={{ minWidth: isMobile ? 130 : 200 }}
onChange={(value) => {
const opt = SORT_OPTIONS.find((o) => o.value === value);
@@ -1709,7 +1714,10 @@ export default function ClientsPage() {
locale={{
emptyText: (
),
@@ -1734,7 +1742,7 @@ export default function ClientsPage() {
)}
{filteredClients.length === 0 && (
)}
@@ -1799,6 +1807,7 @@ export default function ClientsPage() {
checked={!!row.enable}
size="small"
loading={togglingEmail === row.email}
+ aria-label={t('pages.clients.enabled')}
onChange={(next) => onToggleEnable(row, next)}
/>
- {t('pages.clients.qrCode')}
+ {' '}
+ {t('pages.clients.qrCode')}
>
),
onClick: () => onShowQr(row.email),
@@ -1819,7 +1829,7 @@ export default function ClientsPage() {
key: 'reset',
label: (
<>
- {' '}
+ {' '}
{t('pages.inbounds.resetTraffic')}
>
),
@@ -1829,7 +1839,7 @@ export default function ClientsPage() {
key: 'edit',
label: (
<>
- {t('edit')}
+ {t('edit')}
>
),
onClick: () => onEdit(row.email),
@@ -1839,7 +1849,7 @@ export default function ClientsPage() {
danger: true,
label: (
<>
- {t('delete')}
+ {t('delete')}
>
),
onClick: () => onDelete(row.email),
@@ -1851,7 +1861,7 @@ export default function ClientsPage() {
type="text"
size="small"
className="row-action-trigger"
- icon={}
+ icon={}
aria-label={t('more')}
/>
diff --git a/frontend/src/pages/clients/FilterDrawer.tsx b/frontend/src/pages/clients/FilterDrawer.tsx
index 3b0050016..9ee926adc 100644
--- a/frontend/src/pages/clients/FilterDrawer.tsx
+++ b/frontend/src/pages/clients/FilterDrawer.tsx
@@ -1,4 +1,4 @@
-import { useMemo } from 'react';
+import { useId, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import {
Button,
@@ -46,6 +46,7 @@ export default function FilterDrawer({
nodes,
}: FilterDrawerProps) {
const { t } = useTranslation();
+ const uid = useId();
function patch(key: K, value: ClientFilters[K]) {
onChange({ ...filters, [key]: value });
@@ -113,8 +114,9 @@ export default function FilterDrawer({
-
+
-
+
{nodes.length > 0 && (
-
+
)}
-
+
-
+
{
const from = range?.[0]?.startOf('day').valueOf();
@@ -182,10 +188,11 @@ export default function FilterDrawer({
/>
-
+
}
+ icon={}
aria-label={t('pages.clients.qrCode')}
onClick={() => onShowQr(email)}
/>
@@ -54,7 +54,7 @@ export const ClientRowActions = memo(function ClientRowActions({
size="small"
type="text"
style={ICON_BUTTON_STYLE}
- icon={}
+ icon={}
aria-label={t('pages.clients.clientInfo')}
onClick={() => onShowInfo(email)}
/>
@@ -64,7 +64,7 @@ export const ClientRowActions = memo(function ClientRowActions({
size="small"
type="text"
style={ICON_BUTTON_STYLE}
- icon={}
+ icon={}
aria-label={t('pages.inbounds.resetTraffic')}
onClick={() => onResetTraffic(email)}
/>
@@ -74,7 +74,7 @@ export const ClientRowActions = memo(function ClientRowActions({
size="small"
type="text"
style={ICON_BUTTON_STYLE}
- icon={}
+ icon={}
aria-label={t('edit')}
onClick={() => onEdit(email)}
/>
@@ -85,7 +85,7 @@ export const ClientRowActions = memo(function ClientRowActions({
type="text"
danger
style={ICON_BUTTON_STYLE}
- icon={}
+ icon={}
aria-label={t('delete')}
onClick={() => onDelete(email)}
/>
diff --git a/frontend/src/pages/clients/SubLinksModal.tsx b/frontend/src/pages/clients/SubLinksModal.tsx
index 012b42a9a..83b8064db 100644
--- a/frontend/src/pages/clients/SubLinksModal.tsx
+++ b/frontend/src/pages/clients/SubLinksModal.tsx
@@ -122,7 +122,7 @@ export default function SubLinksModal({
size="small"
type="text"
aria-label={t('copy')}
- icon={}
+ icon={}
onClick={() => copy(row.link, t('copied'))}
/>
),
@@ -157,7 +157,7 @@ export default function SubLinksModal({
onOpenChange(false)}>{t('close')}
}
+ icon={ }
disabled={rows.length === 0}
onClick={() =>
copy(allText, t('pages.clients.subLinksCopiedAll', { count: rows.length }))
@@ -167,7 +167,7 @@ export default function SubLinksModal({
}
+ icon={ }
disabled={rows.length === 0}
onClick={download}
>
diff --git a/frontend/src/pages/groups/GroupAddClientsModal.tsx b/frontend/src/pages/groups/GroupAddClientsModal.tsx
index a64c95e03..5655cd38a 100644
--- a/frontend/src/pages/groups/GroupAddClientsModal.tsx
+++ b/frontend/src/pages/groups/GroupAddClientsModal.tsx
@@ -138,6 +138,7 @@ export default function GroupAddClientsModal({
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder={t('pages.inbounds.attachClientsSearchPlaceholder')}
+ aria-label={t('pages.inbounds.attachClientsSearchPlaceholder')}
style={{ maxWidth: 320 }}
/>
diff --git a/frontend/src/pages/groups/GroupRemoveClientsModal.tsx b/frontend/src/pages/groups/GroupRemoveClientsModal.tsx
index 4c63c82eb..c462c8a49 100644
--- a/frontend/src/pages/groups/GroupRemoveClientsModal.tsx
+++ b/frontend/src/pages/groups/GroupRemoveClientsModal.tsx
@@ -122,6 +122,7 @@ export default function GroupRemoveClientsModal({
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder={t('pages.inbounds.attachClientsSearchPlaceholder')}
+ aria-label={t('pages.inbounds.attachClientsSearchPlaceholder')}
style={{ maxWidth: 320 }}
/>
diff --git a/frontend/src/pages/groups/GroupsPage.tsx b/frontend/src/pages/groups/GroupsPage.tsx
index e223ba1e9..5663ba162 100644
--- a/frontend/src/pages/groups/GroupsPage.tsx
+++ b/frontend/src/pages/groups/GroupsPage.tsx
@@ -1,4 +1,4 @@
-import { lazy, useCallback, useEffect, useMemo, useState } from 'react';
+import { lazy, useCallback, useEffect, useId, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
Button,
@@ -89,6 +89,7 @@ async function fetchEmailsForGroup(name: string): Promise {
export default function GroupsPage() {
usePageTitle();
const { t } = useTranslation();
+ const uid = useId();
const { isDark, isUltra, antdThemeConfig } = useTheme();
const { isMobile } = useMediaQuery();
const [modal, modalContextHolder] = Modal.useModal();
@@ -353,41 +354,41 @@ export default function GroupsPage() {
return [
{
key: 'subLinks',
- icon: ,
+ icon: ,
label: t('pages.clients.subLinksSelected', { count: row.clientCount || 0 }),
disabled: !row.clientCount,
onClick: () => openSubLinksFor(row),
},
{
key: 'adjust',
- icon: ,
+ icon: ,
label: t('pages.clients.adjustSelected', { count: row.clientCount || 0 }),
disabled: !row.clientCount,
onClick: () => openAdjustFor(row),
},
{
key: 'reset',
- icon: ,
+ icon: ,
label: t('pages.groups.resetTraffic'),
disabled: !row.clientCount,
onClick: () => onResetTraffic(row),
},
{
key: 'addClients',
- icon: ,
+ icon: ,
label: t('pages.groups.addToGroup'),
onClick: () => openAddClientsFor(row),
},
{
key: 'rename',
- icon: ,
+ icon: ,
label: t('pages.groups.rename'),
onClick: () => openRename(row),
},
{ type: 'divider' },
{
key: 'removeClients',
- icon: ,
+ icon: ,
label: t('pages.groups.removeFromGroup'),
danger: true,
disabled: !row.clientCount,
@@ -395,7 +396,7 @@ export default function GroupsPage() {
},
{
key: 'deleteClients',
- icon: ,
+ icon: ,
label: t('pages.groups.deleteClients'),
danger: true,
disabled: !row.clientCount,
@@ -403,7 +404,7 @@ export default function GroupsPage() {
},
{
key: 'delete',
- icon: ,
+ icon: ,
label: t('pages.groups.deleteGroupOnly'),
danger: true,
onClick: () => onDelete(row),
@@ -424,7 +425,7 @@ export default function GroupsPage() {
size="small"
type="text"
style={{ fontSize: 16 }}
- icon={}
+ icon={}
/>
@@ -433,7 +434,7 @@ export default function GroupsPage() {
size="small"
type="text"
style={{ fontSize: 16 }}
- icon={}
+ icon={}
onClick={() => openRename(row)}
/>
@@ -518,14 +519,14 @@ export default function GroupsPage() {
}
+ prefix={}
/>
}
+ prefix={}
/>
@@ -534,9 +535,11 @@ export default function GroupsPage() {
value={0}
formatter={() => (
- {SizeFormatter.sizeFormat(totalUpload)}
+ {' '}
+ {SizeFormatter.sizeFormat(totalUpload)}
{' / '}
- {SizeFormatter.sizeFormat(totalDownload)}
+ {' '}
+ {SizeFormatter.sizeFormat(totalDownload)}
)}
/>
@@ -545,7 +548,7 @@ export default function GroupsPage() {
}
+ prefix={}
/>
@@ -561,7 +564,7 @@ export default function GroupsPage() {
}
+ icon={}
onClick={openCreate}
>
{!isMobile && t('pages.groups.addGroup')}
@@ -579,7 +582,7 @@ export default function GroupsPage() {
locale={{
emptyText: (
),
@@ -604,8 +607,9 @@ export default function GroupsPage() {
destroyOnHidden
>
+
setCreateName(e.target.value)}
onPressEnter={confirmCreate}
@@ -627,8 +631,9 @@ export default function GroupsPage() {
destroyOnHidden
>
+
setRenameValue(e.target.value)}
onPressEnter={confirmRename}
diff --git a/frontend/src/pages/hosts/HostFormModal.tsx b/frontend/src/pages/hosts/HostFormModal.tsx
index 172e71160..ca2efeebb 100644
--- a/frontend/src/pages/hosts/HostFormModal.tsx
+++ b/frontend/src/pages/hosts/HostFormModal.tsx
@@ -208,7 +208,11 @@ export default function HostFormModal({
{
key: 'basic',
forceRender: true,
- label: catTabLabel(, t('pages.hosts.sections.basic'), isMobile),
+ label: catTabLabel(
+ ,
+ t('pages.hosts.sections.basic'),
+ isMobile,
+ ),
children: (
<>
,
+ ,
t('pages.hosts.sections.security'),
isMobile,
),
@@ -373,7 +377,7 @@ export default function HostFormModal({
key: 'advanced',
forceRender: true,
label: catTabLabel(
- ,
+ ,
t('pages.hosts.sections.advanced'),
isMobile,
),
@@ -386,7 +390,7 @@ export default function HostFormModal({
key: 'adv-general',
forceRender: true,
label: catTabLabel(
- ,
+ ,
t('pages.hosts.sections.general'),
isMobile,
),
@@ -425,7 +429,7 @@ export default function HostFormModal({
key: 'adv-mux',
forceRender: true,
label: catTabLabel(
- ,
+ ,
t('pages.hosts.fields.muxParams'),
isMobile,
),
@@ -445,7 +449,7 @@ export default function HostFormModal({
key: 'adv-sockopt',
forceRender: true,
label: catTabLabel(
- ,
+ ,
t('pages.hosts.fields.sockoptParams'),
isMobile,
),
@@ -465,7 +469,7 @@ export default function HostFormModal({
key: 'adv-finalmask',
forceRender: true,
label: catTabLabel(
- ,
+ ,
t('pages.hosts.fields.finalMask'),
isMobile,
),
@@ -489,7 +493,7 @@ export default function HostFormModal({
key: 'clash',
forceRender: true,
label: catTabLabel(
- ,
+ ,
t('pages.hosts.sections.clash'),
isMobile,
),
diff --git a/frontend/src/pages/hosts/HostList.tsx b/frontend/src/pages/hosts/HostList.tsx
index 77301ca43..b10576b10 100644
--- a/frontend/src/pages/hosts/HostList.tsx
+++ b/frontend/src/pages/hosts/HostList.tsx
@@ -95,7 +95,7 @@ export default function HostList(props: HostListProps) {
}
+ icon={}
aria-label={t('pages.hosts.moveUp')}
disabled={idx === 0}
onClick={() => onMove(h, 'up')}
@@ -105,7 +105,7 @@ export default function HostList(props: HostListProps) {
}
+ icon={}
aria-label={t('pages.hosts.moveDown')}
disabled={idx >= count - 1}
onClick={() => onMove(h, 'down')}
@@ -115,7 +115,7 @@ export default function HostList(props: HostListProps) {
}
+ icon={}
aria-label={t('edit')}
onClick={() => onEdit(h)}
/>
@@ -125,7 +125,7 @@ export default function HostList(props: HostListProps) {
size="small"
type="text"
danger
- icon={}
+ icon={}
aria-label={t('delete')}
onClick={() => onDelete(h)}
/>
@@ -139,7 +139,12 @@ export default function HostList(props: HostListProps) {
key: 'enable',
width: 90,
render: (_, h) => (
- onToggleEnable(h, next)} />
+ onToggleEnable(h, next)}
+ />
),
},
{
@@ -276,7 +281,7 @@ export default function HostList(props: HostListProps) {
const toolbar = (
{selectedGroupIds.length === 0 ? (
- } onClick={onAdd}>
+ } onClick={onAdd}>
{!isMobile && t('pages.hosts.addHost')}
) : (
@@ -291,7 +296,7 @@ export default function HostList(props: HostListProps) {
onBulkEnable(true)}>{t('pages.hosts.bulkEnable')}
onBulkEnable(false)}>{t('pages.hosts.bulkDisable')}
- } onClick={onBulkDelete}>
+ } onClick={onBulkDelete}>
{t('pages.hosts.bulkDelete')}
>
@@ -316,7 +321,7 @@ export default function HostList(props: HostListProps) {
locale={{
emptyText: (
),
diff --git a/frontend/src/pages/hosts/HostsPage.tsx b/frontend/src/pages/hosts/HostsPage.tsx
index c873d1989..4d0045ad7 100644
--- a/frontend/src/pages/hosts/HostsPage.tsx
+++ b/frontend/src/pages/hosts/HostsPage.tsx
@@ -174,7 +174,7 @@ export default function HostsPage() {
}
+ prefix={ }
/>
@@ -182,7 +182,10 @@ export default function HostsPage() {
title={t('pages.hosts.summary.enabled')}
value={String(summary.enabled)}
prefix={
-
+
}
/>
@@ -191,7 +194,10 @@ export default function HostsPage() {
title={t('pages.hosts.summary.disabled')}
value={String(summary.disabled)}
prefix={
-
+
}
/>
diff --git a/frontend/src/pages/inbounds/InboundsPage.tsx b/frontend/src/pages/inbounds/InboundsPage.tsx
index b0f1da7d9..ff1d54691 100644
--- a/frontend/src/pages/inbounds/InboundsPage.tsx
+++ b/frontend/src/pages/inbounds/InboundsPage.tsx
@@ -754,9 +754,11 @@ export default function InboundsPage() {
value={0}
formatter={() => (
- {SizeFormatter.sizeFormat(totals.up)}
+ {' '}
+ {SizeFormatter.sizeFormat(totals.up)}
{' / '}
- {SizeFormatter.sizeFormat(totals.down)}
+ {' '}
+ {SizeFormatter.sizeFormat(totals.down)}
)}
/>
@@ -765,14 +767,14 @@ export default function InboundsPage() {
}
+ prefix={ }
/>
}
+ prefix={ }
/>
diff --git a/frontend/src/pages/inbounds/form/FallbacksCard.tsx b/frontend/src/pages/inbounds/form/FallbacksCard.tsx
index 7b56de9c1..2e7dc6de3 100644
--- a/frontend/src/pages/inbounds/form/FallbacksCard.tsx
+++ b/frontend/src/pages/inbounds/form/FallbacksCard.tsx
@@ -32,7 +32,13 @@ export default function FallbacksCard({
const addButtons = (
- } onClick={addFallback}>
+ }
+ onClick={addFallback}
+ >
{t('pages.inbounds.fallbacks.add') || 'Add fallback'}
moveFallback(idx, -1)}
title={t('pages.inbounds.form.moveUp')}
- icon={}
+ icon={}
/>
moveFallback(idx, 1)}
title={t('pages.inbounds.form.moveDown')}
- icon={}
+ icon={}
/>
removeFallback(idx)}
- icon={}
+ icon={}
/>
(
-
- {label}
-
-
-
-
-);
-
const PROTOCOL_OPTIONS = Object.values(Protocols).map((p) => ({ value: p, label: p }));
const SHARE_ADDR_STRATEGIES = ['node', 'listen', 'custom'] as const;
const SHARE_ADDR_HOSTNAME_RE =
@@ -240,6 +218,7 @@ export default function InboundFormModal({
availableNodesFetched = true,
}: InboundFormModalProps) {
const { t } = useTranslation();
+ const uid = useId();
const [messageApi, messageContextHolder] = message.useMessage();
const [modal, modalContextHolder] = Modal.useModal();
const methods = useForm({ defaultValues: buildAddModeValues() });
@@ -668,7 +647,8 @@ export default function InboundFormModal({
@@ -677,10 +657,8 @@ export default function InboundFormModal({
<>
- {t('pages.inbounds.totalFlow')}
-
- }
+ htmlFor={`${uid}-totalFlow`}
+ label={t('pages.inbounds.totalFlow')}
+ tooltip={t('pages.inbounds.meansNoLimit')}
>
+
+ {t('pages.inbounds.meansNoLimit')}
+
@@ -791,16 +764,19 @@ export default function InboundFormModal({
)}
- {t('pages.inbounds.expireDate')}
-
- }
+ htmlFor={`${uid}-expiryTime`}
+ label={t('pages.inbounds.expireDate')}
+ tooltip={t('pages.inbounds.leaveBlankToNeverExpire')}
>
0 ? dayjs(wExpiry) : null}
onChange={(d) => setV('expiryTime', d ? d.valueOf() : 0)}
/>
+
+ {t('pages.inbounds.leaveBlankToNeverExpire')}
+
>
);
@@ -936,8 +912,9 @@ export default function InboundFormModal({
const streamTab = (
<>
{hasSelectableTransport && (
-
+
{fields.length > 0 && (
@@ -32,13 +32,13 @@ export default function AccountsList() {
{String(idx + 1)}
-
+
-
+
remove(idx)}>
-
+
))}
diff --git a/frontend/src/pages/inbounds/form/protocols/amneziawg.tsx b/frontend/src/pages/inbounds/form/protocols/amneziawg.tsx
index 39c13d065..368b6fec5 100644
--- a/frontend/src/pages/inbounds/form/protocols/amneziawg.tsx
+++ b/frontend/src/pages/inbounds/form/protocols/amneziawg.tsx
@@ -1,3 +1,4 @@
+import { useId } from 'react';
import { useTranslation } from 'react-i18next';
import { Button, Form, Input, InputNumber, Space, Switch } from 'antd';
import { ReloadOutlined } from '@ant-design/icons';
@@ -16,22 +17,23 @@ export default function AmneziawgFields({
regenInboundAwgObfuscation,
}: AmneziawgFieldsProps) {
const { t } = useTranslation();
+ const uid = useId();
return (
<>
-
+
-
+
}
+ icon={}
onClick={regenInboundAwg}
/>
-
-
+
+
- } onClick={regenInboundAwgObfuscation}>
+ } onClick={regenInboundAwgObfuscation}>
{t('pages.xray.amneziawg.regenerateObfuscation')}
diff --git a/frontend/src/pages/inbounds/form/protocols/hysteria.tsx b/frontend/src/pages/inbounds/form/protocols/hysteria.tsx
index 83f5aa951..8456e939a 100644
--- a/frontend/src/pages/inbounds/form/protocols/hysteria.tsx
+++ b/frontend/src/pages/inbounds/form/protocols/hysteria.tsx
@@ -1,3 +1,4 @@
+import { useId } from 'react';
import { useTranslation } from 'react-i18next';
import { Form, Input, InputNumber, Select, Switch } from 'antd';
import { useFormContext, useWatch } from 'react-hook-form';
@@ -9,6 +10,7 @@ const MASQ_PATH = ['streamSettings', 'hysteriaSettings', 'masquerade'];
export default function HysteriaFields() {
const { t } = useTranslation();
+ const uid = useId();
const { control, setValue } = useFormContext();
const masq = useWatch({ control, name: 'streamSettings.hysteriaSettings.masquerade' }) as
| { type?: string }
@@ -32,8 +34,9 @@ export default function HysteriaFields() {
-
+
setValue(
diff --git a/frontend/src/pages/inbounds/form/protocols/shadowsocks.tsx b/frontend/src/pages/inbounds/form/protocols/shadowsocks.tsx
index 2c82b122c..df0ddd817 100644
--- a/frontend/src/pages/inbounds/form/protocols/shadowsocks.tsx
+++ b/frontend/src/pages/inbounds/form/protocols/shadowsocks.tsx
@@ -1,3 +1,4 @@
+import { useId } from 'react';
import { useTranslation } from 'react-i18next';
import { Button, Form, Input, Select, Space, Switch } from 'antd';
import { ReloadOutlined } from '@ant-design/icons';
@@ -13,6 +14,7 @@ interface ShadowsocksFieldsProps {
export default function ShadowsocksFields({ isSSWith2022 }: ShadowsocksFieldsProps) {
const { t } = useTranslation();
+ const uid = useId();
const { getValues, setValue } = useFormContext();
return (
<>
@@ -26,14 +28,14 @@ export default function ShadowsocksFields({ isSSWith2022 }: ShadowsocksFieldsPro
- } onClick={autofillFromSni}>
+ } onClick={autofillFromSni}>
{t('pages.inbounds.form.autoFill')}
{t('pages.xray.tuic.profileOptions')}
-
+
handleSniChange(e.target.value)}
/>
+
+ {t('pages.xray.tuic.sniHint')}
+
string;
}
-function StringList({ name, label, placeholder }: StringListProps) {
+function StringList({ name, label, tooltip, placeholder }: StringListProps) {
const { t } = useTranslation();
+ const labelId = useId();
const { control } = useFormContext();
const { fields, append, remove } = useFieldArray({ control, name: name.join('.') });
return (
-
+ {label}} tooltip={tooltip}>
append('')}>
-
+
{fields.map((field, j) => (
-
+
remove(j)}>
-
+
))}
+ {tooltip && (
+
+ {tooltip}
+
+ )}
);
}
@@ -73,20 +84,14 @@ export default function TunFields() {
- {t('pages.inbounds.info.autoSystemRoutes')}
-
- }
+ label={t('pages.inbounds.info.autoSystemRoutes')}
+ tooltip={t('pages.inbounds.form.autoSystemRoutesTooltip')}
placeholder={(j) => (j === 0 ? '0.0.0.0/0' : '::/0')}
/>
- {t('pages.inbounds.form.autoOutboundsInterface')}
-
- }
+ label={t('pages.inbounds.form.autoOutboundsInterface')}
+ tooltip={t('pages.inbounds.form.autoOutboundsInterfaceTooltip')}
>
diff --git a/frontend/src/pages/inbounds/form/protocols/vless.tsx b/frontend/src/pages/inbounds/form/protocols/vless.tsx
index 628e364e6..c82b04730 100644
--- a/frontend/src/pages/inbounds/form/protocols/vless.tsx
+++ b/frontend/src/pages/inbounds/form/protocols/vless.tsx
@@ -1,4 +1,4 @@
-import { useState } from 'react';
+import { useState, useId } from 'react';
import { useTranslation } from 'react-i18next';
import { Button, Form, Input, InputNumber, Select, Space, Typography } from 'antd';
import { Controller, useFormContext } from 'react-hook-form';
@@ -26,6 +26,7 @@ export default function VlessFields({
clearVlessEnc,
}: VlessFieldsProps) {
const { t } = useTranslation();
+ const uid = useId();
const { control } = useFormContext();
const [authKind, setAuthKind] = useState(vlessAuthKind ?? 'x25519');
@@ -47,9 +48,10 @@ export default function VlessFields({
-
+
- } onClick={() => setScannerOpen(true)}>
+ } onClick={() => setScannerOpen(true)}>
{t('pages.inbounds.form.findTargets')}
+
+ {t('pages.inbounds.form.realityTargetHint')}
+
{scanResult && (
@@ -235,32 +245,45 @@ export default function RealityForm({
>
-
+
-
+
}
+ icon={}
onClick={randomizeShortIds}
/>
-
+
}
+ icon={}
onClick={randomizeSpiderX}
/>
+
+ {t('pages.inbounds.form.spiderXHint')}
+
1 && (
- {t('remove')}
+ {t('remove')}
)}
@@ -161,16 +162,20 @@ function CertRow({
function EchSockoptSection() {
const { t } = useTranslation();
+ const uid = useId();
const { control, setValue } = useFormContext();
const echSockopt = useWatch({ control, name: 'streamSettings.tlsSettings.echSockopt' });
const on = !!echSockopt;
return (
<>
setValue(
@@ -179,6 +184,9 @@ function EchSockoptSection() {
)
}
/>
+
+ {t('pages.inbounds.form.echSockoptTip')}
+
{on && (
<>
@@ -226,6 +234,7 @@ export default function TlsForm({
clearEchCert,
}: TlsFormProps) {
const { t } = useTranslation();
+ const uid = useId();
const { control } = useFormContext();
const { fields, append, remove } = useFieldArray({
control,
@@ -242,16 +251,18 @@ export default function TlsForm({
>
-
+
{fields.map((field, idx) => (
@@ -375,6 +386,7 @@ export default function TlsForm({
@@ -384,6 +396,8 @@ export default function TlsForm({
noStyle
>
}
+ icon={}
onClick={pinFromCert}
loading={saving}
+ aria-label={t('pages.inbounds.form.pinFromCert')}
title={t('pages.inbounds.form.pinFromCert')}
/>
}
+ icon={}
onClick={() => pinFromRemote()}
loading={saving}
+ aria-label={t('pages.inbounds.form.pinFromRemote')}
title={t('pages.inbounds.form.pinFromRemote')}
/>
+
+ {t('pages.inbounds.form.pinnedPeerCertSha256Tip')}
+
-
+
{
setValue(
diff --git a/frontend/src/pages/inbounds/form/transport/sockopt.tsx b/frontend/src/pages/inbounds/form/transport/sockopt.tsx
index 269c4d565..398b8d899 100644
--- a/frontend/src/pages/inbounds/form/transport/sockopt.tsx
+++ b/frontend/src/pages/inbounds/form/transport/sockopt.tsx
@@ -1,3 +1,4 @@
+import { useId } from 'react';
import { useTranslation } from 'react-i18next';
import { Alert, Form, InputNumber, Segmented, Select, Switch } from 'antd';
import { Controller, useFormContext, useWatch } from 'react-hook-form';
@@ -28,6 +29,7 @@ export default function SockoptForm({
network: string;
}) {
const { t } = useTranslation();
+ const uid = useId();
const { control, getValues, setValue } = useFormContext();
const sock = useWatch({ control, name: 'streamSettings.sockopt' });
const on = !!sock && typeof sock === 'object' && Object.keys(sock).length > 0;
@@ -101,6 +103,8 @@ export default function SockoptForm({
tooltip={t('pages.inbounds.form.realClientIpHint')}
>
applyRealClientIpPreset(v as RealClientIpPreset)}
options={[
@@ -112,6 +116,9 @@ export default function SockoptForm({
{ value: 'proxy', label: t('pages.inbounds.form.realClientIpPresetProxyProtocol') },
]}
/>
+
+ {t('pages.inbounds.form.realClientIpHint')}
+
{trustedMismatch && (
}
+ icon={}
aria-label={t('copy')}
onClick={() => copyText(clientSettings.tgId, t)}
/>
@@ -552,7 +552,7 @@ export default function InboundInfoModal({
}
+ icon={}
aria-label={t('copy')}
onClick={() => copyText(link.link, t)}
/>
@@ -573,7 +573,7 @@ export default function InboundInfoModal({
}
+ icon={}
aria-label={t('copy')}
onClick={() => copyText(subLink, t)}
/>
@@ -595,7 +595,7 @@ export default function InboundInfoModal({
}
+ icon={}
aria-label={t('copy')}
onClick={() => copyText(subJsonLink, t)}
/>
@@ -714,7 +714,7 @@ export default function InboundInfoModal({
}
+ icon={}
aria-label={t('copy')}
onClick={() => copyText(encryptionLabel, t)}
/>
@@ -984,7 +984,7 @@ export default function InboundInfoModal({
}
+ icon={}
aria-label={t('copy')}
onClick={() => copyText(`${account.user}:${account.pass}`, t)}
/>
@@ -1100,7 +1100,7 @@ export default function InboundInfoModal({
}
+ icon={}
aria-label={t('copy')}
onClick={() => copyText(`${account.user}:${account.pass}`, t)}
/>
@@ -1201,7 +1201,7 @@ export default function InboundInfoModal({
}
+ icon={}
aria-label={t('copy')}
onClick={() => copyText(cfg, t)}
/>
@@ -1209,7 +1209,7 @@ export default function InboundInfoModal({
}
+ icon={}
aria-label={t('download')}
onClick={() =>
downloadText(cfg, peerConfFileName(idx, j, all.length))
@@ -1230,7 +1230,7 @@ export default function InboundInfoModal({
}
+ icon={}
aria-label={t('copy')}
onClick={() => copyText(link, t)}
/>
@@ -1261,7 +1261,7 @@ export default function InboundInfoModal({
}
+ icon={}
aria-label={t('copy')}
onClick={() => copyText(cfg, t)}
/>
@@ -1269,7 +1269,7 @@ export default function InboundInfoModal({
}
+ icon={}
aria-label={t('download')}
onClick={() => downloadText(cfg, peerConfFileName(idx, j, all.length))}
/>
@@ -1288,7 +1288,7 @@ export default function InboundInfoModal({
}
+ icon={}
aria-label={t('copy')}
onClick={() => copyText(link, t)}
/>
diff --git a/frontend/src/pages/inbounds/list/InboundList.tsx b/frontend/src/pages/inbounds/list/InboundList.tsx
index 33748a4d8..bad9144a0 100644
--- a/frontend/src/pages/inbounds/list/InboundList.tsx
+++ b/frontend/src/pages/inbounds/list/InboundList.tsx
@@ -193,20 +193,24 @@ export default function InboundList({
const generalActionsMenu: MenuProps = {
items: [
- { key: 'import', icon: , label: t('pages.inbounds.importInbound') },
- { key: 'export', icon: , label: t('pages.inbounds.export') },
+ {
+ key: 'import',
+ icon: ,
+ label: t('pages.inbounds.importInbound'),
+ },
+ { key: 'export', icon: , label: t('pages.inbounds.export') },
...(subEnable
? [
{
key: 'subs',
- icon: ,
+ icon: ,
label: `${t('pages.inbounds.export')} — ${t('pages.settings.subSettings')}`,
},
]
: []),
{
key: 'resetInbounds',
- icon: ,
+ icon: ,
label: t('pages.inbounds.resetAllTraffic'),
},
],
@@ -221,7 +225,7 @@ export default function InboundList({
}
+ icon={}
aria-label={t('pages.inbounds.addInbound')}
>
{!isMobile && t('pages.inbounds.addInbound')}
@@ -229,7 +233,7 @@ export default function InboundList({
}
+ icon={}
aria-label={t('pages.inbounds.generalActions')}
>
{!isMobile && t('pages.inbounds.generalActions')}
@@ -251,7 +255,7 @@ export default function InboundList({
onChange={(e) => setSearchKey(e.target.value)}
placeholder={t('search')}
allowClear
- prefix={}
+ prefix={}
style={{ maxWidth: isMobile ? 110 : 200 }}
aria-label={t('search')}
/>
@@ -267,7 +271,7 @@ export default function InboundList({
}
+ icon={}
onClick={handleBulkDelete}
aria-label={t('delete')}
>
@@ -283,7 +287,7 @@ export default function InboundList({
{visibleInbounds.length === 0 ? (
) : (
@@ -327,6 +331,7 @@ export default function InboundList({
/>
onSwitchEnable(record, next)}
@@ -350,7 +355,7 @@ export default function InboundList({
type="text"
size="small"
className="row-action-trigger"
- icon={}
+ icon={}
aria-label={t('more')}
/>
@@ -377,7 +382,7 @@ export default function InboundList({
locale={{
emptyText: (
),
diff --git a/frontend/src/pages/inbounds/list/RowActions.tsx b/frontend/src/pages/inbounds/list/RowActions.tsx
index 1529782e6..08fc51fcf 100644
--- a/frontend/src/pages/inbounds/list/RowActions.tsx
+++ b/frontend/src/pages/inbounds/list/RowActions.tsx
@@ -40,69 +40,82 @@ export function buildRowActionsMenu({
}): MenuProps['items'] {
const items: MenuProps['items'] = [];
if (isMobile) {
- items.push({ key: 'edit', icon: , label: t('edit') });
+ items.push({ key: 'edit', icon: , label: t('edit') });
}
if (isInboundMultiUser(record)) {
- items.push({ key: 'export', icon: , label: t('pages.inbounds.export') });
+ items.push({
+ key: 'export',
+ icon: ,
+ label: t('pages.inbounds.export'),
+ });
if (subEnable) {
items.push({
key: 'subs',
- icon: ,
+ icon: ,
label: `${t('pages.inbounds.export')} — ${t('pages.settings.subSettings')}`,
});
}
} else {
items.push({
key: 'showInfo',
- icon: ,
+ icon: ,
label: t('pages.inbounds.inboundInfo'),
});
}
items.push({
key: 'clipboard',
- icon: ,
+ icon: ,
label: t('pages.inbounds.exportInbound'),
});
items.push({
key: 'resetTraffic',
- icon: ,
+ icon: ,
label: t('pages.inbounds.resetTraffic'),
});
- items.push({ key: 'clone', icon: , label: t('pages.inbounds.clone') });
+ items.push({
+ key: 'clone',
+ icon: ,
+ label: t('pages.inbounds.clone'),
+ });
if (isInboundMultiUser(record)) {
items.push({
key: 'attachExisting',
- icon: ,
+ icon: ,
label: t('pages.inbounds.attachExistingClients'),
});
}
if (isInboundMultiUser(record) && hasClients) {
items.push({
key: 'attachClients',
- icon: ,
+ icon: ,
label: t('pages.inbounds.attachClients'),
});
items.push({
key: 'detachClients',
- icon: ,
+ icon: ,
label: t('pages.inbounds.detachClients'),
});
items.push({
key: 'addToGroup',
- icon: ,
+ icon: ,
label: t('pages.inbounds.addClientsToGroup'),
});
items.push({ type: 'divider' });
items.push({
key: 'delAllClients',
- icon: ,
+ icon: ,
danger: true,
label: t('pages.inbounds.delAllClients'),
});
} else {
items.push({ type: 'divider' });
}
- items.push({ key: 'delete', icon: , danger: true, label: t('delete') });
+ items.push({
+ key: 'delete',
+ icon: ,
+ danger: true,
+ label: t('delete'),
+ });
return items;
}
@@ -114,7 +127,7 @@ export function RowActionsCell({ record, subEnable, hasClients, onClick }: RowAc
type="text"
size="small"
style={{ fontSize: 16 }}
- icon={}
+ icon={}
aria-label={t('edit')}
onClick={() => onClick('edit')}
/>
@@ -129,7 +142,7 @@ export function RowActionsCell({ record, subEnable, hasClients, onClick }: RowAc
type="text"
size="small"
style={{ fontSize: 16 }}
- icon={}
+ icon={}
aria-label={t('more')}
/>
diff --git a/frontend/src/pages/inbounds/list/useInboundColumns.tsx b/frontend/src/pages/inbounds/list/useInboundColumns.tsx
index 9f08e868c..1d2a7c8bb 100644
--- a/frontend/src/pages/inbounds/list/useInboundColumns.tsx
+++ b/frontend/src/pages/inbounds/list/useInboundColumns.tsx
@@ -130,7 +130,11 @@ export function useInboundColumns({
align: 'center',
width: 80,
render: (_, record) => (
- onSwitchEnable(record, next)} />
+ onSwitchEnable(record, next)}
+ />
),
},
];
@@ -293,7 +297,7 @@ export function useInboundColumns({
className="client-count-tag"
style={{ margin: 0, marginRight: 4, padding: '0 2px' }}
>
- {cc.clients}
+ {cc.clients}
{cc.active.length > 0 ? (
- } aria-label={t('copy')} onClick={copy} />
+ }
+ aria-label={t('copy')}
+ onClick={copy}
+ />
{showQr && (
}
+ icon={}
aria-label={t('downloadImage')}
onClick={downloadImage}
/>
@@ -118,7 +123,7 @@ export default function QrPanel({
}
+ icon={}
aria-label={t('download')}
onClick={download}
/>
diff --git a/frontend/src/pages/index/AmneziaWGLogModal.tsx b/frontend/src/pages/index/AmneziaWGLogModal.tsx
index cf6c1cfe5..b78ba0d8d 100644
--- a/frontend/src/pages/index/AmneziaWGLogModal.tsx
+++ b/frontend/src/pages/index/AmneziaWGLogModal.tsx
@@ -1,4 +1,4 @@
-import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { useCallback, useEffect, useMemo, useRef, useState, useId } from 'react';
import { useTranslation } from 'react-i18next';
import { Button, Checkbox, Empty, Form, Input, Modal, Select, Tag } from 'antd';
import { DownloadOutlined, SyncOutlined } from '@ant-design/icons';
@@ -30,6 +30,7 @@ function shortTime(value?: number): string {
export default function AmneziaWGLogModal({ open, onClose }: AmneziaWGLogModalProps) {
const { t } = useTranslation();
+ const uid = useId();
const { datepicker } = useDatepicker();
const { isMobile } = useMediaQuery();
const [rows, setRows] = useState('50');
@@ -120,6 +121,7 @@ export default function AmneziaWGLogModal({ open, onClose }: AmneziaWGLogModalPr
-
+
setFilter(e.target.value)}
@@ -151,7 +154,7 @@ export default function AmneziaWGLogModal({ open, onClose }: AmneziaWGLogModalPr
}
+ icon={}
aria-label={t('download')}
/>
diff --git a/frontend/src/pages/index/BackupModal.tsx b/frontend/src/pages/index/BackupModal.tsx
index a77478923..473d30592 100644
--- a/frontend/src/pages/index/BackupModal.tsx
+++ b/frontend/src/pages/index/BackupModal.tsx
@@ -88,7 +88,7 @@ export default function BackupModal({
type="primary"
aria-label={t('pages.index.exportDatabase')}
onClick={exportDb}
- icon={}
+ icon={}
/>
@@ -102,7 +102,7 @@ export default function BackupModal({
type="primary"
aria-label={t('pages.index.migrationDownload')}
onClick={exportMigration}
- icon={}
+ icon={}
/>
)}
@@ -120,7 +120,7 @@ export default function BackupModal({
type="primary"
aria-label={t('pages.index.importDatabase')}
onClick={importDb}
- icon={}
+ icon={}
/>
diff --git a/frontend/src/pages/index/GeodataSection.tsx b/frontend/src/pages/index/GeodataSection.tsx
index cfe2237dd..4af726925 100644
--- a/frontend/src/pages/index/GeodataSection.tsx
+++ b/frontend/src/pages/index/GeodataSection.tsx
@@ -1,4 +1,4 @@
-import { useCallback, useEffect, useRef, useState } from 'react';
+import { useCallback, useEffect, useRef, useState, useId } from 'react';
import { useTranslation } from 'react-i18next';
import { Alert, Button, Form, Input, Modal, Select, Space, Spin, Typography, message } from 'antd';
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons';
@@ -33,6 +33,7 @@ function fileNameFromUrl(url: string): string {
export default function GeodataSection({ active, onBusy, onClose }: GeodataSectionProps) {
const { t } = useTranslation();
+ const uid = useId();
const [modal, modalContextHolder] = Modal.useModal();
const [messageApi, messageContextHolder] = message.useMessage();
const [loading, setLoading] = useState(false);
@@ -180,15 +181,25 @@ export default function GeodataSection({ active, onBusy, onClose }: GeodataSecti
+
setCron(e.target.value)}
/>
-
+
|