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:
風雪 committed 2026-08-18 09:05:18 +08:00
1 parent 01c72f2c3e
commit 96b3d862ca
1 file changed
+63 -116
+63 -116
View File
@@ -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>