|
|
@@ -12,6 +12,7 @@ import {
|
|
|
Select,
|
|
|
Space,
|
|
|
Switch,
|
|
|
+ Tabs,
|
|
|
Tag,
|
|
|
message,
|
|
|
} from 'antd';
|
|
|
@@ -64,7 +65,6 @@ interface ClientFormModalProps {
|
|
|
client: ClientRecord | null;
|
|
|
inbounds: InboundOption[];
|
|
|
attachedIds?: number[];
|
|
|
- ipLimitEnable?: boolean;
|
|
|
tgBotEnable?: boolean;
|
|
|
groups?: string[];
|
|
|
save: (
|
|
|
@@ -136,7 +136,6 @@ export default function ClientFormModal({
|
|
|
client,
|
|
|
inbounds,
|
|
|
attachedIds = [],
|
|
|
- ipLimitEnable = false,
|
|
|
tgBotEnable = false,
|
|
|
groups = [],
|
|
|
save,
|
|
|
@@ -424,214 +423,232 @@ export default function ClientFormModal({
|
|
|
onCancel={close}
|
|
|
>
|
|
|
<Form layout="vertical">
|
|
|
- <Row gutter={16}>
|
|
|
- <Col xs={24} md={12}>
|
|
|
- <Form.Item label={t('pages.clients.email')} required>
|
|
|
- <Space.Compact style={{ display: 'flex' }}>
|
|
|
- <Input
|
|
|
- value={form.email}
|
|
|
- placeholder={t('pages.clients.email')}
|
|
|
- style={{ flex: 1 }}
|
|
|
- onChange={(e) => update('email', e.target.value)}
|
|
|
- />
|
|
|
- <Button icon={<ReloadOutlined />} onClick={() => update('email', RandomUtil.randomLowerAndNum(12))} />
|
|
|
- </Space.Compact>
|
|
|
- </Form.Item>
|
|
|
- </Col>
|
|
|
- <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>
|
|
|
- </Row>
|
|
|
-
|
|
|
- <Row gutter={16}>
|
|
|
- <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>
|
|
|
- <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}>
|
|
|
- <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={ipLimitEnable ? 8 : 12}>
|
|
|
- <Form.Item label={t('pages.clients.totalGB')}>
|
|
|
- <InputNumber value={form.totalGB} min={0} step={1} style={{ width: '100%' }}
|
|
|
- onChange={(v) => update('totalGB', Number(v) || 0)} />
|
|
|
- </Form.Item>
|
|
|
- </Col>
|
|
|
- {ipLimitEnable && (
|
|
|
- <Col xs={24} md={4}>
|
|
|
- <Form.Item label={t('pages.clients.limitIp')}>
|
|
|
- <InputNumber value={form.limitIp} min={0} style={{ width: '100%' }}
|
|
|
- onChange={(v) => update('limitIp', Number(v) || 0)} />
|
|
|
- </Form.Item>
|
|
|
- </Col>
|
|
|
- )}
|
|
|
- </Row>
|
|
|
-
|
|
|
- <Row gutter={16}>
|
|
|
- <Col xs={24} md={12}>
|
|
|
- {form.delayedStart ? (
|
|
|
- <Form.Item label={t('pages.clients.expireDays')}>
|
|
|
- <InputNumber value={form.delayedDays} min={0} style={{ width: '100%' }}
|
|
|
- onChange={(v) => update('delayedDays', Number(v) || 0)} />
|
|
|
- </Form.Item>
|
|
|
- ) : (
|
|
|
- <Form.Item label={t('pages.clients.expiryTime')}>
|
|
|
- <DateTimePicker
|
|
|
- value={form.expiryDate}
|
|
|
- onChange={(d) => update('expiryDate', d || null)}
|
|
|
- />
|
|
|
- </Form.Item>
|
|
|
- )}
|
|
|
- </Col>
|
|
|
- <Col xs={24} md={12}>
|
|
|
- <Form.Item label={t('pages.clients.delayedStart')}>
|
|
|
- <Switch
|
|
|
- checked={form.delayedStart}
|
|
|
- onChange={(v) => {
|
|
|
- update('delayedStart', v);
|
|
|
- if (v) update('expiryDate', null);
|
|
|
- else update('delayedDays', 0);
|
|
|
- }}
|
|
|
- />
|
|
|
- </Form.Item>
|
|
|
- </Col>
|
|
|
- </Row>
|
|
|
-
|
|
|
- <Row gutter={16}>
|
|
|
- <Col xs={24} md={12}>
|
|
|
- <Form.Item
|
|
|
- label={t('pages.clients.renew')}
|
|
|
- tooltip={t('pages.clients.renewDesc')}
|
|
|
- >
|
|
|
- <InputNumber value={form.reset} min={0} style={{ width: '100%' }}
|
|
|
- onChange={(v) => update('reset', Number(v) || 0)} />
|
|
|
- </Form.Item>
|
|
|
- </Col>
|
|
|
- {showReverseTag && (
|
|
|
- <Col xs={24} md={12}>
|
|
|
- <Form.Item label={t('pages.clients.reverseTag')}>
|
|
|
- <Input value={form.reverseTag} placeholder={t('pages.clients.reverseTagPlaceholder')}
|
|
|
- onChange={(e) => update('reverseTag', e.target.value)} />
|
|
|
- </Form.Item>
|
|
|
- </Col>
|
|
|
- )}
|
|
|
- {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>
|
|
|
-
|
|
|
- <Row gutter={16}>
|
|
|
- {tgBotEnable && (
|
|
|
- <Col xs={24} md={12}>
|
|
|
- <Form.Item label={t('pages.clients.telegramId')}>
|
|
|
- <InputNumber value={form.tgId} min={0} controls={false}
|
|
|
- placeholder={t('pages.clients.telegramIdPlaceholder')} style={{ width: '100%' }}
|
|
|
- onChange={(v) => update('tgId', Number(v) || 0)} />
|
|
|
- </Form.Item>
|
|
|
- </Col>
|
|
|
- )}
|
|
|
- <Col xs={24} md={tgBotEnable ? 12 : 24}>
|
|
|
- <Form.Item label={t('pages.clients.comment')}>
|
|
|
- <Input value={form.comment} onChange={(e) => update('comment', e.target.value)} />
|
|
|
- </Form.Item>
|
|
|
- </Col>
|
|
|
- <Col xs={24} md={12}>
|
|
|
- <Form.Item label={t('pages.clients.group')} tooltip={t('pages.clients.groupDesc')}>
|
|
|
- <AutoComplete
|
|
|
- value={form.group}
|
|
|
- placeholder={t('pages.clients.groupPlaceholder')}
|
|
|
- options={groups.map((g) => ({ value: g }))}
|
|
|
- onChange={(v) => update('group', v ?? '')}
|
|
|
- filterOption={(input, option) =>
|
|
|
- String(option?.value ?? '').toLowerCase().includes((input || '').toLowerCase())
|
|
|
- }
|
|
|
- allowClear
|
|
|
- style={{ width: '100%' }}
|
|
|
- />
|
|
|
- </Form.Item>
|
|
|
- </Col>
|
|
|
- </Row>
|
|
|
-
|
|
|
- <Form.Item label={t('pages.clients.attachedInbounds')} required={!isEdit}>
|
|
|
- <SelectAllClearButtons
|
|
|
- options={inboundOptions}
|
|
|
- value={form.inboundIds}
|
|
|
- onChange={(v) => update('inboundIds', v)}
|
|
|
- />
|
|
|
- <Select
|
|
|
- mode="multiple"
|
|
|
- value={form.inboundIds}
|
|
|
- onChange={(v) => update('inboundIds', v)}
|
|
|
- options={inboundOptions}
|
|
|
- placeholder={t('pages.clients.selectInbound')}
|
|
|
- maxTagCount="responsive"
|
|
|
- placement="topLeft"
|
|
|
- listHeight={220}
|
|
|
- showSearch={{
|
|
|
- filterOption: (input, option) => ((option?.label as string) || '').toLowerCase().includes(input.toLowerCase()),
|
|
|
- }}
|
|
|
- />
|
|
|
- </Form.Item>
|
|
|
-
|
|
|
- <Form.Item>
|
|
|
- <Switch checked={form.enable} onChange={(v) => update('enable', v)} />
|
|
|
- <span style={{ marginLeft: 8 }}>{t('enable')}</span>
|
|
|
- </Form.Item>
|
|
|
-
|
|
|
- {isEdit && ipLimitEnable && (
|
|
|
- <Form.Item label={t('pages.clients.ipLog')}>
|
|
|
- <Button icon={<EyeOutlined />} loading={ipsLoading} onClick={openIpsModal}>
|
|
|
- {clientIps.length > 0 ? clientIps.length : ''}
|
|
|
- </Button>
|
|
|
- </Form.Item>
|
|
|
- )}
|
|
|
+ <Tabs
|
|
|
+ defaultActiveKey="basic"
|
|
|
+ items={[
|
|
|
+ {
|
|
|
+ key: 'basic',
|
|
|
+ label: t('pages.clients.tabBasic'),
|
|
|
+ children: (
|
|
|
+ <>
|
|
|
+ <Row gutter={16}>
|
|
|
+ <Col xs={24} md={12}>
|
|
|
+ <Form.Item label={t('pages.clients.email')} required>
|
|
|
+ <Space.Compact style={{ display: 'flex' }}>
|
|
|
+ <Input
|
|
|
+ value={form.email}
|
|
|
+ placeholder={t('pages.clients.email')}
|
|
|
+ style={{ flex: 1 }}
|
|
|
+ onChange={(e) => update('email', e.target.value)}
|
|
|
+ />
|
|
|
+ <Button icon={<ReloadOutlined />} onClick={() => update('email', RandomUtil.randomLowerAndNum(12))} />
|
|
|
+ </Space.Compact>
|
|
|
+ </Form.Item>
|
|
|
+ </Col>
|
|
|
+ <Col xs={24} md={8}>
|
|
|
+ <Form.Item label={t('pages.clients.totalGB')}>
|
|
|
+ <InputNumber value={form.totalGB} min={0} step={1} style={{ width: '100%' }}
|
|
|
+ onChange={(v) => update('totalGB', Number(v) || 0)} />
|
|
|
+ </Form.Item>
|
|
|
+ </Col>
|
|
|
+ <Col xs={24} md={4}>
|
|
|
+ <Form.Item label={t('pages.clients.limitIp')}>
|
|
|
+ <InputNumber value={form.limitIp} min={0} style={{ width: '100%' }}
|
|
|
+ onChange={(v) => update('limitIp', Number(v) || 0)} />
|
|
|
+ </Form.Item>
|
|
|
+ </Col>
|
|
|
+ </Row>
|
|
|
+
|
|
|
+ <Row gutter={16}>
|
|
|
+ <Col xs={24} md={12}>
|
|
|
+ {form.delayedStart ? (
|
|
|
+ <Form.Item label={t('pages.clients.expireDays')}>
|
|
|
+ <InputNumber value={form.delayedDays} min={0} style={{ width: '100%' }}
|
|
|
+ onChange={(v) => update('delayedDays', Number(v) || 0)} />
|
|
|
+ </Form.Item>
|
|
|
+ ) : (
|
|
|
+ <Form.Item label={t('pages.clients.expiryTime')}>
|
|
|
+ <DateTimePicker
|
|
|
+ value={form.expiryDate}
|
|
|
+ onChange={(d) => update('expiryDate', d || null)}
|
|
|
+ />
|
|
|
+ </Form.Item>
|
|
|
+ )}
|
|
|
+ </Col>
|
|
|
+ <Col xs={12} md={6}>
|
|
|
+ <Form.Item label={t('pages.clients.delayedStart')}>
|
|
|
+ <Switch
|
|
|
+ checked={form.delayedStart}
|
|
|
+ onChange={(v) => {
|
|
|
+ update('delayedStart', v);
|
|
|
+ if (v) update('expiryDate', null);
|
|
|
+ else update('delayedDays', 0);
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ </Form.Item>
|
|
|
+ </Col>
|
|
|
+ <Col xs={12} md={6}>
|
|
|
+ <Form.Item
|
|
|
+ label={t('pages.clients.renew')}
|
|
|
+ tooltip={t('pages.clients.renewDesc')}
|
|
|
+ >
|
|
|
+ <InputNumber value={form.reset} min={0} style={{ width: '100%' }}
|
|
|
+ onChange={(v) => update('reset', Number(v) || 0)} />
|
|
|
+ </Form.Item>
|
|
|
+ </Col>
|
|
|
+ </Row>
|
|
|
+
|
|
|
+ <Row gutter={16}>
|
|
|
+ {tgBotEnable && (
|
|
|
+ <Col xs={24} md={12}>
|
|
|
+ <Form.Item label={t('pages.clients.telegramId')}>
|
|
|
+ <InputNumber value={form.tgId} min={0} controls={false}
|
|
|
+ placeholder={t('pages.clients.telegramIdPlaceholder')} style={{ width: '100%' }}
|
|
|
+ onChange={(v) => update('tgId', Number(v) || 0)} />
|
|
|
+ </Form.Item>
|
|
|
+ </Col>
|
|
|
+ )}
|
|
|
+ <Col xs={24} md={tgBotEnable ? 12 : 24}>
|
|
|
+ <Form.Item label={t('pages.clients.comment')}>
|
|
|
+ <Input value={form.comment} onChange={(e) => update('comment', e.target.value)} />
|
|
|
+ </Form.Item>
|
|
|
+ </Col>
|
|
|
+ <Col xs={24} md={12}>
|
|
|
+ <Form.Item label={t('pages.clients.group')} tooltip={t('pages.clients.groupDesc')}>
|
|
|
+ <AutoComplete
|
|
|
+ value={form.group}
|
|
|
+ placeholder={t('pages.clients.groupPlaceholder')}
|
|
|
+ options={groups.map((g) => ({ value: g }))}
|
|
|
+ onChange={(v) => update('group', v ?? '')}
|
|
|
+ filterOption={(input, option) =>
|
|
|
+ String(option?.value ?? '').toLowerCase().includes((input || '').toLowerCase())
|
|
|
+ }
|
|
|
+ allowClear
|
|
|
+ style={{ width: '100%' }}
|
|
|
+ />
|
|
|
+ </Form.Item>
|
|
|
+ </Col>
|
|
|
+ </Row>
|
|
|
+
|
|
|
+ <Form.Item label={t('pages.clients.attachedInbounds')} required={!isEdit}>
|
|
|
+ <SelectAllClearButtons
|
|
|
+ options={inboundOptions}
|
|
|
+ value={form.inboundIds}
|
|
|
+ onChange={(v) => update('inboundIds', v)}
|
|
|
+ />
|
|
|
+ <Select
|
|
|
+ mode="multiple"
|
|
|
+ value={form.inboundIds}
|
|
|
+ onChange={(v) => update('inboundIds', v)}
|
|
|
+ options={inboundOptions}
|
|
|
+ placeholder={t('pages.clients.selectInbound')}
|
|
|
+ maxTagCount="responsive"
|
|
|
+ placement="topLeft"
|
|
|
+ listHeight={220}
|
|
|
+ showSearch={{
|
|
|
+ filterOption: (input, option) => ((option?.label as string) || '').toLowerCase().includes(input.toLowerCase()),
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Form.Item>
|
|
|
+ <Switch checked={form.enable} onChange={(v) => update('enable', v)} />
|
|
|
+ <span style={{ marginLeft: 8 }}>{t('enable')}</span>
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ {isEdit && (
|
|
|
+ <Form.Item label={t('pages.clients.ipLog')}>
|
|
|
+ <Button icon={<EyeOutlined />} loading={ipsLoading} onClick={openIpsModal}>
|
|
|
+ {clientIps.length > 0 ? clientIps.length : ''}
|
|
|
+ </Button>
|
|
|
+ </Form.Item>
|
|
|
+ )}
|
|
|
+ </>
|
|
|
+ ),
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: 'config',
|
|
|
+ label: t('pages.clients.tabConfig'),
|
|
|
+ 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>
|
|
|
+
|
|
|
+ <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>
|
|
|
+
|
|
|
+ <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>
|
|
|
+ )}
|
|
|
+ {showReverseTag && (
|
|
|
+ <Col xs={24} md={12}>
|
|
|
+ <Form.Item label={t('pages.clients.reverseTag')}>
|
|
|
+ <Input value={form.reverseTag} placeholder={t('pages.clients.reverseTagPlaceholder')}
|
|
|
+ onChange={(e) => update('reverseTag', e.target.value)} />
|
|
|
+ </Form.Item>
|
|
|
+ </Col>
|
|
|
+ )}
|
|
|
+ </Row>
|
|
|
+ </>
|
|
|
+ ),
|
|
|
+ },
|
|
|
+ ]}
|
|
|
+ />
|
|
|
</Form>
|
|
|
</Modal>
|
|
|
|