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:
MHSanaei
2026-06-15 21:38:11 +02:00
parent dc781b28c4
commit bbab83db17
4 changed files with 66 additions and 71 deletions

View File

@ -3,6 +3,7 @@ export type OnlineAPISupport = number;
export type ProcessState = string;
export type Protocol = string;
export type SubLinkProvider = unknown;
export type staticEgressResolver = string;
export type transportBits = number;
export interface AllSetting {

View File

@ -12,6 +12,9 @@ export type Protocol = z.infer<typeof ProtocolSchema>;
export const SubLinkProviderSchema = z.unknown();
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 type transportBits = z.infer<typeof transportBitsSchema>;

View File

@ -678,71 +678,55 @@ export default function ClientFormModal({
label: t('pages.clients.tabCredentials'),
children: (
<>
<Row gutter={16}>
<Col xs={24} md={12}>
<Form.Item label={t('pages.clients.uuid')}>
<Space.Compact style={{ display: 'flex' }}>
<Input value={form.uuid} style={{ flex: 1 }} onChange={(e) => update('uuid', e.target.value)} />
<Button icon={<ReloadOutlined />} onClick={() => update('uuid', RandomUtil.randomUUID())} />
</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>
<Form.Item label={t('pages.clients.uuid')}>
<Space.Compact style={{ display: 'flex' }}>
<Input value={form.uuid} style={{ flex: 1 }} onChange={(e) => update('uuid', e.target.value)} />
<Button icon={<ReloadOutlined />} onClick={() => update('uuid', RandomUtil.randomUUID())} />
</Space.Compact>
</Form.Item>
<Row gutter={16}>
<Col xs={24} md={12}>
<Form.Item label={t('pages.clients.subId')}>
<Space.Compact style={{ display: 'flex' }}>
<Input value={form.subId} style={{ flex: 1 }} onChange={(e) => update('subId', e.target.value)} />
<Button icon={<ReloadOutlined />} onClick={() => update('subId', RandomUtil.randomLowerAndNum(16))} />
</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>
<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>
<Row gutter={16}>
{showFlow && (
<Col xs={24} md={12}>
<Form.Item label={t('pages.clients.flow')}>
<Select
value={form.flow}
onChange={(v) => update('flow', v)}
options={[
{ value: '', label: t('none') },
...FLOW_OPTIONS.map((k) => ({ value: k, label: k })),
]}
/>
</Form.Item>
</Col>
)}
{showSecurity && (
<Col xs={24} md={12}>
<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>
</Col>
)}
</Row>
<Form.Item label={t('pages.clients.subId')}>
<Space.Compact style={{ display: 'flex' }}>
<Input value={form.subId} style={{ flex: 1 }} onChange={(e) => update('subId', e.target.value)} />
<Button icon={<ReloadOutlined />} onClick={() => update('subId', RandomUtil.randomLowerAndNum(16))} />
</Space.Compact>
</Form.Item>
<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>
{showFlow && (
<Form.Item label={t('pages.clients.flow')}>
<Select
value={form.flow}
onChange={(v) => update('flow', v)}
options={[
{ value: '', label: t('none') },
...FLOW_OPTIONS.map((k) => ({ value: k, label: k })),
]}
/>
</Form.Item>
)}
{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>
)}
</>
),
},

View File

@ -1,5 +1,6 @@
import { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { QuestionCircleOutlined } from '@ant-design/icons';
import dayjs from 'dayjs';
import {
Alert,
@ -83,6 +84,16 @@ import type { DBInbound } from '@/models/dbinbound';
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 TRAFFIC_RESETS = ['never', 'hourly', 'daily', 'weekly', 'monthly'] as const;
const SHARE_ADDR_STRATEGIES = ['node', 'listen', 'custom'] as const;
@ -538,16 +549,14 @@ export default function InboundFormModal({
<Form.Item
name="listen"
label={t('pages.inbounds.address')}
extra={t('pages.inbounds.form.listenHelp')}
label={labelWithHint(t('pages.inbounds.address'), t('pages.inbounds.form.listenHelp'))}
>
<Input placeholder={t('pages.inbounds.monitorDesc')} />
</Form.Item>
<Form.Item
name="shareAddrStrategy"
label={t('pages.inbounds.form.shareAddrStrategy')}
extra={t('pages.inbounds.form.shareAddrStrategyHelp')}
label={labelWithHint(t('pages.inbounds.form.shareAddrStrategy'), t('pages.inbounds.form.shareAddrStrategyHelp'))}
>
<Select
options={SHARE_ADDR_STRATEGIES
@ -562,8 +571,7 @@ export default function InboundFormModal({
{shareAddrStrategy === 'custom' && (
<Form.Item
name="shareAddr"
label={t('pages.inbounds.form.shareAddr')}
extra={t('pages.inbounds.form.shareAddrHelp')}
label={labelWithHint(t('pages.inbounds.form.shareAddr'), t('pages.inbounds.form.shareAddrHelp'))}
rules={[{
validator: (_, value) => (
isValidShareAddrInput(String(value ?? ''))
@ -578,8 +586,7 @@ export default function InboundFormModal({
<Form.Item
name="subSortIndex"
label={t('pages.inbounds.form.subSortIndex')}
extra={t('pages.inbounds.form.subSortIndexHelp')}
label={labelWithHint(t('pages.inbounds.form.subSortIndex'), t('pages.inbounds.form.subSortIndexHelp'))}
>
<InputNumber min={1} />
</Form.Item>