mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-27 05:27:14 +00:00
refactor(frontend): stack client credential fields and use label hints on inbound form
Stack UUID/password/subId/auth/flow/security fields vertically in the client modal instead of two-column rows, and replace the inbound form's 'extra' help lines with hover tooltip hints on field labels.
This commit is contained in:
@@ -3,6 +3,7 @@ export type OnlineAPISupport = number;
|
|||||||
export type ProcessState = string;
|
export type ProcessState = string;
|
||||||
export type Protocol = string;
|
export type Protocol = string;
|
||||||
export type SubLinkProvider = unknown;
|
export type SubLinkProvider = unknown;
|
||||||
|
export type staticEgressResolver = string;
|
||||||
export type transportBits = number;
|
export type transportBits = number;
|
||||||
|
|
||||||
export interface AllSetting {
|
export interface AllSetting {
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ export type Protocol = z.infer<typeof ProtocolSchema>;
|
|||||||
export const SubLinkProviderSchema = z.unknown();
|
export const SubLinkProviderSchema = z.unknown();
|
||||||
export type SubLinkProvider = z.infer<typeof SubLinkProviderSchema>;
|
export type SubLinkProvider = z.infer<typeof SubLinkProviderSchema>;
|
||||||
|
|
||||||
|
export const staticEgressResolverSchema = z.string();
|
||||||
|
export type staticEgressResolver = z.infer<typeof staticEgressResolverSchema>;
|
||||||
|
|
||||||
export const transportBitsSchema = z.number().int();
|
export const transportBitsSchema = z.number().int();
|
||||||
export type transportBits = z.infer<typeof transportBitsSchema>;
|
export type transportBits = z.infer<typeof transportBitsSchema>;
|
||||||
|
|
||||||
|
|||||||
@@ -678,71 +678,55 @@ export default function ClientFormModal({
|
|||||||
label: t('pages.clients.tabCredentials'),
|
label: t('pages.clients.tabCredentials'),
|
||||||
children: (
|
children: (
|
||||||
<>
|
<>
|
||||||
<Row gutter={16}>
|
<Form.Item label={t('pages.clients.uuid')}>
|
||||||
<Col xs={24} md={12}>
|
<Space.Compact style={{ display: 'flex' }}>
|
||||||
<Form.Item label={t('pages.clients.uuid')}>
|
<Input value={form.uuid} style={{ flex: 1 }} onChange={(e) => update('uuid', e.target.value)} />
|
||||||
<Space.Compact style={{ display: 'flex' }}>
|
<Button icon={<ReloadOutlined />} onClick={() => update('uuid', RandomUtil.randomUUID())} />
|
||||||
<Input value={form.uuid} style={{ flex: 1 }} onChange={(e) => update('uuid', e.target.value)} />
|
</Space.Compact>
|
||||||
<Button icon={<ReloadOutlined />} onClick={() => update('uuid', RandomUtil.randomUUID())} />
|
</Form.Item>
|
||||||
</Space.Compact>
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
<Col xs={24} md={12}>
|
|
||||||
<Form.Item label={t('pages.clients.password')}>
|
|
||||||
<Space.Compact style={{ display: 'flex' }}>
|
|
||||||
<Input value={form.password} style={{ flex: 1 }} onChange={(e) => update('password', e.target.value)} />
|
|
||||||
<Button icon={<ReloadOutlined />} onClick={regeneratePassword} />
|
|
||||||
</Space.Compact>
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
|
|
||||||
<Row gutter={16}>
|
<Form.Item label={t('pages.clients.password')}>
|
||||||
<Col xs={24} md={12}>
|
<Space.Compact style={{ display: 'flex' }}>
|
||||||
<Form.Item label={t('pages.clients.subId')}>
|
<Input value={form.password} style={{ flex: 1 }} onChange={(e) => update('password', e.target.value)} />
|
||||||
<Space.Compact style={{ display: 'flex' }}>
|
<Button icon={<ReloadOutlined />} onClick={regeneratePassword} />
|
||||||
<Input value={form.subId} style={{ flex: 1 }} onChange={(e) => update('subId', e.target.value)} />
|
</Space.Compact>
|
||||||
<Button icon={<ReloadOutlined />} onClick={() => update('subId', RandomUtil.randomLowerAndNum(16))} />
|
</Form.Item>
|
||||||
</Space.Compact>
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
<Col xs={24} md={12}>
|
|
||||||
<Form.Item label={t('pages.clients.hysteriaAuth')}>
|
|
||||||
<Space.Compact style={{ display: 'flex' }}>
|
|
||||||
<Input value={form.auth} style={{ flex: 1 }} onChange={(e) => update('auth', e.target.value)} />
|
|
||||||
<Button icon={<ReloadOutlined />} onClick={() => update('auth', RandomUtil.randomLowerAndNum(16))} />
|
|
||||||
</Space.Compact>
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
|
|
||||||
<Row gutter={16}>
|
<Form.Item label={t('pages.clients.subId')}>
|
||||||
{showFlow && (
|
<Space.Compact style={{ display: 'flex' }}>
|
||||||
<Col xs={24} md={12}>
|
<Input value={form.subId} style={{ flex: 1 }} onChange={(e) => update('subId', e.target.value)} />
|
||||||
<Form.Item label={t('pages.clients.flow')}>
|
<Button icon={<ReloadOutlined />} onClick={() => update('subId', RandomUtil.randomLowerAndNum(16))} />
|
||||||
<Select
|
</Space.Compact>
|
||||||
value={form.flow}
|
</Form.Item>
|
||||||
onChange={(v) => update('flow', v)}
|
|
||||||
options={[
|
<Form.Item label={t('pages.clients.hysteriaAuth')}>
|
||||||
{ value: '', label: t('none') },
|
<Space.Compact style={{ display: 'flex' }}>
|
||||||
...FLOW_OPTIONS.map((k) => ({ value: k, label: k })),
|
<Input value={form.auth} style={{ flex: 1 }} onChange={(e) => update('auth', e.target.value)} />
|
||||||
]}
|
<Button icon={<ReloadOutlined />} onClick={() => update('auth', RandomUtil.randomLowerAndNum(16))} />
|
||||||
/>
|
</Space.Compact>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
|
||||||
)}
|
{showFlow && (
|
||||||
{showSecurity && (
|
<Form.Item label={t('pages.clients.flow')}>
|
||||||
<Col xs={24} md={12}>
|
<Select
|
||||||
<Form.Item label={t('pages.clients.vmessSecurity')}>
|
value={form.flow}
|
||||||
<Select
|
onChange={(v) => update('flow', v)}
|
||||||
value={form.security}
|
options={[
|
||||||
onChange={(v) => update('security', v)}
|
{ value: '', label: t('none') },
|
||||||
options={VMESS_SECURITY_OPTIONS.map((k) => ({ value: k, label: k }))}
|
...FLOW_OPTIONS.map((k) => ({ value: k, label: k })),
|
||||||
/>
|
]}
|
||||||
</Form.Item>
|
/>
|
||||||
</Col>
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
</Row>
|
{showSecurity && (
|
||||||
|
<Form.Item label={t('pages.clients.vmessSecurity')}>
|
||||||
|
<Select
|
||||||
|
value={form.security}
|
||||||
|
onChange={(v) => update('security', v)}
|
||||||
|
options={VMESS_SECURITY_OPTIONS.map((k) => ({ value: k, label: k }))}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { QuestionCircleOutlined } from '@ant-design/icons';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
@@ -83,6 +84,16 @@ import type { DBInbound } from '@/models/dbinbound';
|
|||||||
import type { NodeRecord } from '@/api/queries/useNodesQuery';
|
import type { NodeRecord } from '@/api/queries/useNodesQuery';
|
||||||
|
|
||||||
|
|
||||||
|
// Render a field label with a hover tooltip icon instead of an `extra` help line below.
|
||||||
|
const labelWithHint = (label: string, hint: string) => (
|
||||||
|
<span>
|
||||||
|
{label}
|
||||||
|
<Tooltip title={hint}>
|
||||||
|
<QuestionCircleOutlined style={{ marginInlineStart: 4, color: 'rgba(128,128,128,0.65)' }} />
|
||||||
|
</Tooltip>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
const PROTOCOL_OPTIONS = Object.values(Protocols).map((p) => ({ value: p, label: p }));
|
const PROTOCOL_OPTIONS = Object.values(Protocols).map((p) => ({ value: p, label: p }));
|
||||||
const TRAFFIC_RESETS = ['never', 'hourly', 'daily', 'weekly', 'monthly'] as const;
|
const TRAFFIC_RESETS = ['never', 'hourly', 'daily', 'weekly', 'monthly'] as const;
|
||||||
const SHARE_ADDR_STRATEGIES = ['node', 'listen', 'custom'] as const;
|
const SHARE_ADDR_STRATEGIES = ['node', 'listen', 'custom'] as const;
|
||||||
@@ -538,16 +549,14 @@ export default function InboundFormModal({
|
|||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="listen"
|
name="listen"
|
||||||
label={t('pages.inbounds.address')}
|
label={labelWithHint(t('pages.inbounds.address'), t('pages.inbounds.form.listenHelp'))}
|
||||||
extra={t('pages.inbounds.form.listenHelp')}
|
|
||||||
>
|
>
|
||||||
<Input placeholder={t('pages.inbounds.monitorDesc')} />
|
<Input placeholder={t('pages.inbounds.monitorDesc')} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="shareAddrStrategy"
|
name="shareAddrStrategy"
|
||||||
label={t('pages.inbounds.form.shareAddrStrategy')}
|
label={labelWithHint(t('pages.inbounds.form.shareAddrStrategy'), t('pages.inbounds.form.shareAddrStrategyHelp'))}
|
||||||
extra={t('pages.inbounds.form.shareAddrStrategyHelp')}
|
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
options={SHARE_ADDR_STRATEGIES
|
options={SHARE_ADDR_STRATEGIES
|
||||||
@@ -562,8 +571,7 @@ export default function InboundFormModal({
|
|||||||
{shareAddrStrategy === 'custom' && (
|
{shareAddrStrategy === 'custom' && (
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="shareAddr"
|
name="shareAddr"
|
||||||
label={t('pages.inbounds.form.shareAddr')}
|
label={labelWithHint(t('pages.inbounds.form.shareAddr'), t('pages.inbounds.form.shareAddrHelp'))}
|
||||||
extra={t('pages.inbounds.form.shareAddrHelp')}
|
|
||||||
rules={[{
|
rules={[{
|
||||||
validator: (_, value) => (
|
validator: (_, value) => (
|
||||||
isValidShareAddrInput(String(value ?? ''))
|
isValidShareAddrInput(String(value ?? ''))
|
||||||
@@ -578,8 +586,7 @@ export default function InboundFormModal({
|
|||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="subSortIndex"
|
name="subSortIndex"
|
||||||
label={t('pages.inbounds.form.subSortIndex')}
|
label={labelWithHint(t('pages.inbounds.form.subSortIndex'), t('pages.inbounds.form.subSortIndexHelp'))}
|
||||||
extra={t('pages.inbounds.form.subSortIndexHelp')}
|
|
||||||
>
|
>
|
||||||
<InputNumber min={1} />
|
<InputNumber min={1} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
Reference in New Issue
Block a user