ui(listeners): restore single-page create/edit form (remove wizard)
ui(listeners): restore single-page create/edit form (remove wizard) ui(listeners): restore single-page create/edit form (remove wizard) ui(listeners): restore single-page create/edit form (remove wizard) ui(listeners): restore single-page create/edit form (remove wizard) ui(listeners): restore single-page create/edit form (remove wizard) ui(listeners): restore single-page create/edit form (remove wizard) ui(listeners): restore single-page create/edit form (remove wizard) ui(listeners): restore single-page create/edit form (remove wizard) ui(listeners): restore single-page create/edit form (remove wizard) ui(listeners): restore single-page create/edit form (remove wizard) ui(listeners): restore single-page create/edit form (remove wizard)
This commit is contained in:
1 parent
01c72f2c3e
commit
96b3d862ca
1 file changed
+63
-116
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Table, Button, Space, Tag, Modal, Form, Input, Select, Switch, message, Popconfirm, Tooltip, Card, Tabs, Descriptions, Divider, Steps } from 'antd';
|
||||
import { Table, Button, Space, Tag, Modal, Form, Input, Select, Switch, message, Popconfirm, Tooltip, Card, Tabs, Descriptions, Divider } from 'antd';
|
||||
import { PlusOutlined, ReloadOutlined, QrcodeOutlined, DeleteOutlined, EditOutlined, CopyOutlined, BranchesOutlined, HistoryOutlined, SaveOutlined, PoweroffOutlined, DiffOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
fetchListeners, createListener, updateListener, deleteListener, reloadListener, exportNodeURI, normalizeId, Listener,
|
||||
@@ -45,7 +45,6 @@ const Listeners: React.FC = () => {
|
||||
const [diffText, setDiffText] = useState('');
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
||||
const protocol = Form.useWatch('protocol', form);
|
||||
const [wizardStep, setWizardStep] = useState(0);
|
||||
|
||||
const load = async () => {
|
||||
setLoading(true);
|
||||
@@ -59,31 +58,26 @@ const Listeners: React.FC = () => {
|
||||
|
||||
const openCreate = () => {
|
||||
setEditing(null);
|
||||
setWizardStep(0);
|
||||
form.resetFields();
|
||||
form.setFieldsValue({ bind_address: '0.0.0.0', enabled: true, udp: false, protocol: 'vless' });
|
||||
setModalOpen(true);
|
||||
};
|
||||
const openEdit = (record: Listener) => {
|
||||
setEditing(record);
|
||||
setWizardStep(0);
|
||||
form.resetFields();
|
||||
form.setFieldsValue({ name: record.name, protocol: record.protocol, port: record.port, bind_address: record.bind_address || '0.0.0.0', enabled: record.enabled, udp: record.udp, ...configToFormValues(record.config) });
|
||||
setModalOpen(true);
|
||||
};
|
||||
const onSubmit = async (rawValues?: any) => {
|
||||
try {
|
||||
// Wizard unmounts prior steps; always read full store (including preserved fields).
|
||||
const values = { ...(form.getFieldsValue(true) || {}), ...(rawValues || {}) };
|
||||
const proto = String(values.protocol || '').trim();
|
||||
if (!proto) {
|
||||
message.error(t('listeners.selectProtocolFirst'));
|
||||
setWizardStep(0);
|
||||
return;
|
||||
}
|
||||
if (!values.name || !String(values.port || '').trim()) {
|
||||
message.error(t('listeners.portHint') || 'Name and port are required');
|
||||
setWizardStep(0);
|
||||
return;
|
||||
}
|
||||
const previous = editing ? parseConfig(editing.config) : null;
|
||||
@@ -97,10 +91,20 @@ const Listeners: React.FC = () => {
|
||||
udp: protocolSupportsUDP(proto) ? !!values.udp : false,
|
||||
config: JSON.stringify(config),
|
||||
};
|
||||
if (editing) { await updateListener(normalizeId(editing), payload); message.success(t('listeners.updated')); }
|
||||
else { await createListener(payload); message.success(t('listeners.created')); }
|
||||
setModalOpen(false); setEditing(null); setWizardStep(0); form.resetFields(); await load();
|
||||
} catch (e: any) { message.error(e.message); }
|
||||
if (editing) {
|
||||
await updateListener(normalizeId(editing), payload);
|
||||
message.success(t('listeners.updated'));
|
||||
} else {
|
||||
await createListener(payload);
|
||||
message.success(t('listeners.created'));
|
||||
}
|
||||
setModalOpen(false);
|
||||
setEditing(null);
|
||||
form.resetFields();
|
||||
await load();
|
||||
} catch (e: any) {
|
||||
message.error(e.message);
|
||||
}
|
||||
};
|
||||
const onDelete = async (id: number) => { try { await deleteListener(id); message.success(t('listeners.deleted')); await load(); } catch (e: any) { message.error(e.message); } };
|
||||
const onReload = async (id: number) => { try { await reloadListener(id); message.success(t('listeners.reloaded')); await load(); } catch (e: any) { message.error(e.message); } };
|
||||
@@ -187,116 +191,59 @@ const Listeners: React.FC = () => {
|
||||
|
||||
<Modal
|
||||
open={modalOpen}
|
||||
title={editing ? t('listeners.edit') : (t('listeners.createWizard') || t('listeners.create'))}
|
||||
onCancel={() => { setModalOpen(false); setEditing(null); setWizardStep(0); form.resetFields(); }}
|
||||
width={typeof window !== 'undefined' && window.innerWidth < 768 ? '100%' : 760}
|
||||
title={editing ? t('listeners.edit') : t('listeners.create')}
|
||||
onCancel={() => { setModalOpen(false); setEditing(null); form.resetFields(); }}
|
||||
onOk={() => form.submit()}
|
||||
width={typeof window !== 'undefined' && window.innerWidth < 768 ? '100%' : 720}
|
||||
destroyOnClose
|
||||
styles={{ body: { maxHeight: '70vh', overflowY: 'auto' } }}
|
||||
footer={
|
||||
editing ? (
|
||||
<Space>
|
||||
<Button onClick={() => { setModalOpen(false); setEditing(null); form.resetFields(); }}>{t('common.cancel')}</Button>
|
||||
<Button type="primary" onClick={() => form.submit()}>{t('common.save')}</Button>
|
||||
</Space>
|
||||
) : (
|
||||
<Space>
|
||||
<Button onClick={() => { setModalOpen(false); setWizardStep(0); form.resetFields(); }}>{t('common.cancel')}</Button>
|
||||
{wizardStep > 0 && (
|
||||
<Button onClick={() => setWizardStep((s) => Math.max(0, s - 1))}>{t('common.prev') || 'Previous'}</Button>
|
||||
)}
|
||||
{wizardStep < 2 ? (
|
||||
<Button type="primary" onClick={async () => {
|
||||
try {
|
||||
if (wizardStep === 0) {
|
||||
await form.validateFields(['name', 'protocol', 'port', 'bind_address']);
|
||||
}
|
||||
setWizardStep((s) => s + 1);
|
||||
} catch { /* validation failed */ }
|
||||
}}>{t('common.next') || 'Next'}</Button>
|
||||
) : (
|
||||
<Button type="primary" onClick={() => form.submit()}>{t('listeners.create')}</Button>
|
||||
)}
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
>
|
||||
{!editing && (
|
||||
<Steps
|
||||
size="small"
|
||||
current={wizardStep}
|
||||
style={{ marginBottom: 20 }}
|
||||
items={[
|
||||
{ title: t('listeners.wizardBasic') || 'Basic' },
|
||||
{ title: t('listeners.wizardConfig') || 'Config' },
|
||||
{ title: t('listeners.wizardReview') || 'Review' },
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
<Form form={form} layout="vertical" onFinish={onSubmit} preserve>
|
||||
{(editing || wizardStep === 0) && (
|
||||
<>
|
||||
<Form.Item name="name" label={t('listeners.name')} rules={[{ required: true }]}><Input placeholder="my-vless" /></Form.Item>
|
||||
<Form.Item name="protocol" label={t('listeners.protocol')} rules={[{ required: true }]}>
|
||||
<Select
|
||||
options={PROTOCOLS.map(p => ({ value: p, label: p }))}
|
||||
onChange={(nextProto: string) => {
|
||||
const keep = form.getFieldsValue(['name', 'port', 'bind_address', 'enabled', 'udp']);
|
||||
form.resetFields();
|
||||
form.setFieldsValue({ ...keep, protocol: nextProto });
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="port"
|
||||
label={t('listeners.port')}
|
||||
tooltip={t('listeners.portHint')}
|
||||
rules={[
|
||||
{ required: true, message: t('listeners.portHint') },
|
||||
{
|
||||
validator: async (_, v) => {
|
||||
const s = String(v || '').trim();
|
||||
if (!s) return Promise.reject(new Error(t('listeners.portHint')));
|
||||
// Official: single port or ports syntax e.g. 200,204,401-429
|
||||
if (!/^\d{1,5}([,-]\d{1,5})*$/.test(s.replace(/\s/g, ''))) {
|
||||
return Promise.reject(new Error(t('listeners.portHint')));
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
},
|
||||
]}
|
||||
><Input placeholder="443" /></Form.Item>
|
||||
<Form.Item name="bind_address" label={t('listeners.bindAddress')} initialValue="0.0.0.0"><Input /></Form.Item>
|
||||
<Form.Item name="enabled" label={t('listeners.status')} valuePropName="checked" initialValue={true}><Switch /></Form.Item>
|
||||
{protocolSupportsUDP(protocol) && <Form.Item name="udp" label={t('listeners.udp')} valuePropName="checked" initialValue={false}><Switch /></Form.Item>}
|
||||
</>
|
||||
)}
|
||||
{(editing || wizardStep === 1) && (
|
||||
<div>
|
||||
{!editing && wizardStep === 1 && (
|
||||
<p style={{ color: 'rgba(0,0,0,0.45)', marginBottom: 12 }}>
|
||||
{t('listeners.wizardConfigHint') || 'Configure protocol-specific fields (TLS / Reality / users, etc.).'}
|
||||
</p>
|
||||
)}
|
||||
<ListenerConfigFields protocol={protocol} />
|
||||
</div>
|
||||
)}
|
||||
{!editing && wizardStep === 2 && (
|
||||
<Descriptions column={1} size="small" bordered>
|
||||
<Descriptions.Item label={t('listeners.name')}>{form.getFieldValue('name') || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label={t('listeners.protocol')}><Tag>{form.getFieldValue('protocol') || '-'}</Tag></Descriptions.Item>
|
||||
<Descriptions.Item label={t('listeners.port')}>{form.getFieldValue('port') || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label={t('listeners.bindAddress')}>{form.getFieldValue('bind_address') || '0.0.0.0'}</Descriptions.Item>
|
||||
<Descriptions.Item label={t('listeners.status')}>{form.getFieldValue('enabled') ? t('common.enabled') : t('common.disabled')}</Descriptions.Item>
|
||||
{protocolSupportsUDP(protocol) && (
|
||||
<Descriptions.Item label={t('listeners.udp')}>{form.getFieldValue('udp') ? 'UDP' : '-'}</Descriptions.Item>
|
||||
)}
|
||||
<Descriptions.Item label={t('listeners.wizardConfig') || 'Config'}>
|
||||
<pre style={{ margin: 0, maxHeight: 200, overflow: 'auto', fontSize: 12 }}>
|
||||
{JSON.stringify(formValuesToConfig(form.getFieldValue('protocol') || protocol, form.getFieldsValue(true), null), null, 2)}
|
||||
</pre>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
<Form.Item name="name" label={t('listeners.name')} rules={[{ required: true }]}>
|
||||
<Input placeholder="my-vless" />
|
||||
</Form.Item>
|
||||
<Form.Item name="protocol" label={t('listeners.protocol')} rules={[{ required: true }]}>
|
||||
<Select
|
||||
options={PROTOCOLS.map(p => ({ value: p, label: p }))}
|
||||
onChange={(nextProto: string) => {
|
||||
const keep = form.getFieldsValue(['name', 'port', 'bind_address', 'enabled', 'udp']);
|
||||
form.resetFields();
|
||||
form.setFieldsValue({ ...keep, protocol: nextProto });
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="port"
|
||||
label={t('listeners.port')}
|
||||
tooltip={t('listeners.portHint')}
|
||||
rules={[
|
||||
{ required: true, message: t('listeners.portHint') },
|
||||
{
|
||||
validator: async (_, v) => {
|
||||
const s = String(v || '').trim();
|
||||
if (!s) return Promise.reject(new Error(t('listeners.portHint')));
|
||||
if (!/^\d{1,5}([,-]\d{1,5})*$/.test(s.replace(/\s/g, ''))) {
|
||||
return Promise.reject(new Error(t('listeners.portHint')));
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input placeholder="443" />
|
||||
</Form.Item>
|
||||
<Form.Item name="bind_address" label={t('listeners.bindAddress')} initialValue="0.0.0.0">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="enabled" label={t('listeners.status')} valuePropName="checked" initialValue={true}>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
{protocolSupportsUDP(protocol) && (
|
||||
<Form.Item name="udp" label={t('listeners.udp')} valuePropName="checked" initialValue={false}>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
)}
|
||||
<ListenerConfigFields protocol={protocol} />
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
|
||||
Reference in new issue
Block a user