Преглед изворни кода

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.
MHSanaei пре 12 часа
родитељ
комит
bbab83db17

+ 1 - 0
frontend/src/generated/types.ts

@@ -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 {

+ 3 - 0
frontend/src/generated/zod.ts

@@ -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>;
 

+ 47 - 63
frontend/src/pages/clients/ClientFormModal.tsx

@@ -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>
+                    )}
                   </>
                 ),
               },

+ 15 - 8
frontend/src/pages/inbounds/form/InboundFormModal.tsx

@@ -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>