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
+1
View File
@@ -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 {
+3
View File
@@ -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>;
+47 -63
View File
@@ -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>