diff --git a/src/components/CustomNode.tsx b/src/components/CustomNode.tsx index d3db6bc..2f5d207 100644 --- a/src/components/CustomNode.tsx +++ b/src/components/CustomNode.tsx @@ -12,9 +12,6 @@ export default function CustomNode({
{data.label}
- -
-
{[Position.Top, Position.Bottom, Position.Right, Position.Left].map((position) => ( (["target", "source"] as const).map((type) => ( diff --git a/src/components/EdgeEditor.tsx b/src/components/EdgeEditor.tsx index 88861a0..319c48d 100644 --- a/src/components/EdgeEditor.tsx +++ b/src/components/EdgeEditor.tsx @@ -1,10 +1,11 @@ import { useState, ReactNode } from 'react'; -import { AppNode, AppEdge, EdgeData } from '../types/graph'; +import { AppNode, AppEdge, EdgeData, EdgeDataUpdate } from '../types/graph'; import { useReactFlow } from '@xyflow/react'; +import './FormEditor.css'; interface EdgeEditorProps { edge: EdgeData; - onUpdate: (data: EdgeData) => void; + onUpdate: (data: EdgeDataUpdate) => void; onClose: () => void; } @@ -13,18 +14,12 @@ export default function NodeEditor({ onUpdate, onClose }: EdgeEditorProps): ReactNode { - const [formData, setFormData] = useState(edge); + + const [keepalive, setKeepalive] = useState(edge.persistentKeepalive); const { getNode, getEdge } = useReactFlow(); - const handleInputChange = (field: keyof EdgeData, value: string): void => { - setFormData(prev => ({ - ...prev, - [field]: value - })); - }; - const handleSave = (): void => { - onUpdate(formData); + onUpdate({persistentKeepalive : keepalive}); onClose(); }; @@ -52,8 +47,13 @@ export default function NodeEditor({ type="number" min="0" step="1" - value={formData.persistentKeepalive || ''} - onChange={(e) => handleInputChange('persistentKeepalive', e.target.value)} + value={keepalive || ''} + onChange={ + (e) => { + const value = e.target.valueAsNumber; + setKeepalive(isNaN(value) ? undefined : value); + } + } placeholder={`留空或0代表不保活`} /> diff --git a/src/components/NodeEditor.css b/src/components/FormEditor.css similarity index 95% rename from src/components/NodeEditor.css rename to src/components/FormEditor.css index ee7eaa8..1cf26c3 100644 --- a/src/components/NodeEditor.css +++ b/src/components/FormEditor.css @@ -1,240 +1,240 @@ -.node-editor-overlay { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.5); - display: flex; - align-items: center; - justify-content: center; - z-index: 1000; -} - -.node-editor { - background: white; - border-radius: 8px; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); - max-width: 500px; - width: 90%; - max-height: 90vh; - overflow-y: auto; - padding: 20px; -} - -.editor-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 20px; - border-bottom: 1px solid #eee; - padding-bottom: 15px; -} - -.editor-header h2 { - margin: 0; - font-size: 18px; - color: #333; -} - -.close-btn { - background: none; - border: none; - font-size: 24px; - cursor: pointer; - color: #999; - padding: 0; - width: 30px; - height: 30px; - display: flex; - align-items: center; - justify-content: center; -} - -.close-btn:hover { - color: #333; -} - -.error-box { - position: relative; - background: #fee; - border: 1px solid #fcc; - border-radius: 4px; - padding: 12px; - margin-bottom: 15px; - - animation: shake 0.4s cubic-bezier(.36,.07,.19,.97) both; -} - -.error-close-btn { - position: absolute; - top: 8px; - right: 8px; - width: 20px; - height: 20px; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - color: #ff4d4f; - font-size: 16px; - opacity: 0.6; - transition: opacity 0.2s; - user-select: none; /* 禁止选中 */ -} - -.error-close-btn:hover { - opacity: 1; - background: rgba(255, 77, 79, 0.1); /* 悬停微红 */ - border-radius: 4px; -} - -@keyframes shake { - 10%, 90% { transform: translate3d(-1px, 0, 0); } - 20%, 80% { transform: translate3d(2px, 0, 0); } - 30%, 50%, 70% { transform: translate3d(-4px, 0, 0); } - 40%, 60% { transform: translate3d(4px, 0, 0); } -} - -.error-message { - margin: 5px 0; - color: #c33; - font-size: 14px; -} - -.form-group { - display: flex; - flex-direction: column; - margin-bottom: 15px; -} - -.form-group label { - display: block; - margin-bottom: 5px; - font-weight: 500; - color: #333; - font-size: 14px; -} - -.form-group input, -.form-group textarea { - flex: 1; - padding: 8px; - border: 1px solid #ddd; - border-radius: 4px; - font-size: 14px; - font-family: monospace; -} - -.form-group input:focus, -.form-group textarea:focus { - outline: none; - border-color: #1677ff; - box-shadow: 0 0 0 3px rgba(22, 119, 255, 0.1); -} - -.form-group textarea { - resize: vertical; - font-size: 12px; -} - -.editor-actions { - display: flex; - gap: 10px; - margin-top: 20px; - padding-top: 15px; - border-top: 1px solid #eee; -} - -.item-group { - display: flex; - gap: 10px; -} - -/* 1. 基础通用样式 - 强化了投影和圆角 */ -.btn-interect, .btn-save, .btn-cancel { - padding: 10px 20px; - border: none; - border-radius: 6px; /* 稍微圆润一点,更现代 */ - font-size: 14px; - font-weight: 500; - cursor: pointer; - transition: all 0.2s ease; /* 稍微拉长过渡,显得更平滑 */ - display: flex; - align-items: center; - justify-content: center; - - /* 基础阴影 */ - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); -} - -/* 2. 统一交互反馈:悬停增强阴影,移除缩放 */ -.btn-interect:hover, .btn-save:hover, .btn-cancel:hover { - filter: brightness(1.05); /* 悬停微亮 */ - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12); /* 悬停时投影加深,产生“浮起”感 */ -} - -/* 移除 active 缩放,改为颜色反馈 */ -.btn-interect:active, .btn-save:active, .btn-cancel:active { - filter: brightness(0.95); /* 按下稍微变暗 */ - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - transform: none; /* 明确去掉缩放 */ -} - -.btn-save, .btn-cancel { - flex: 1; -} - -/* 3. 颜色方案优化 */ -.btn-interect, .btn-save { - background: #1677ff; - color: white; - border: 1px solid #0958d9; /* 增加深色边框增强质感 */ -} - -.btn-cancel { - background: #ffffff; /* 改为白色底,配合阴影更好看 */ - color: #4b5563; - border: 1px solid #e5e7eb; /* 浅灰色边框 */ -} - -.btn-cancel:hover { - background: #f9fafb; - color: #1f2937; -} - -/* 补充:调整按钮组的间距,让阴影不被遮挡 */ -.editor-actions { - display: flex; - gap: 12px; - margin-top: 24px; - padding-top: 16px; - border-top: 1px solid #f3f4f6; -} - -/* 移除所有按钮默认的 outline */ -.btn-interect:focus, -.btn-save:focus, -.btn-cancel:focus, -.close-btn:focus { - outline: none; -} - -/* 蓝色按钮的焦点效果 (Save / Interect) */ -.btn-interect:focus-visible, -.btn-save:focus-visible { - box-shadow: 0 0 0 3px rgba(22, 119, 255, 0.3); /* 柔和的蓝色光晕 */ - border-color: #1677ff; -} - -/* 灰色按钮的焦点效果 (Cancel) */ -.btn-cancel:focus-visible { - box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.05); /* 极浅的灰色光晕 */ - border-color: #d1d5db; -} - -/* 关闭按钮的焦点效果 */ -.close-btn:focus-visible { - background-color: #f3f4f6; - border-radius: 4px; +.node-editor-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.node-editor { + background: white; + border-radius: 8px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); + max-width: 500px; + width: 90%; + max-height: 90vh; + overflow-y: auto; + padding: 20px; +} + +.editor-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + border-bottom: 1px solid #eee; + padding-bottom: 15px; +} + +.editor-header h2 { + margin: 0; + font-size: 18px; + color: #333; +} + +.close-btn { + background: none; + border: none; + font-size: 24px; + cursor: pointer; + color: #999; + padding: 0; + width: 30px; + height: 30px; + display: flex; + align-items: center; + justify-content: center; +} + +.close-btn:hover { + color: #333; +} + +.error-box { + position: relative; + background: #fee; + border: 1px solid #fcc; + border-radius: 4px; + padding: 12px; + margin-bottom: 15px; + + animation: shake 0.4s cubic-bezier(.36,.07,.19,.97) both; +} + +.error-close-btn { + position: absolute; + top: 8px; + right: 8px; + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + color: #ff4d4f; + font-size: 16px; + opacity: 0.6; + transition: opacity 0.2s; + user-select: none; /* 禁止选中 */ +} + +.error-close-btn:hover { + opacity: 1; + background: rgba(255, 77, 79, 0.1); /* 悬停微红 */ + border-radius: 4px; +} + +@keyframes shake { + 10%, 90% { transform: translate3d(-1px, 0, 0); } + 20%, 80% { transform: translate3d(2px, 0, 0); } + 30%, 50%, 70% { transform: translate3d(-4px, 0, 0); } + 40%, 60% { transform: translate3d(4px, 0, 0); } +} + +.error-message { + margin: 5px 0; + color: #c33; + font-size: 14px; +} + +.form-group { + display: flex; + flex-direction: column; + margin-bottom: 15px; +} + +.form-group label { + display: block; + margin-bottom: 5px; + font-weight: 500; + color: #333; + font-size: 14px; +} + +.form-group input, +.form-group textarea { + flex: 1; + padding: 8px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 14px; + font-family: monospace; +} + +.form-group input:focus, +.form-group textarea:focus { + outline: none; + border-color: #1677ff; + box-shadow: 0 0 0 3px rgba(22, 119, 255, 0.1); +} + +.form-group textarea { + resize: vertical; + font-size: 12px; +} + +.editor-actions { + display: flex; + gap: 10px; + margin-top: 20px; + padding-top: 15px; + border-top: 1px solid #eee; +} + +.item-group { + display: flex; + gap: 10px; +} + +/* 1. 基础通用样式 - 强化了投影和圆角 */ +.btn-interect, .btn-save, .btn-cancel { + padding: 10px 20px; + border: none; + border-radius: 6px; /* 稍微圆润一点,更现代 */ + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; /* 稍微拉长过渡,显得更平滑 */ + display: flex; + align-items: center; + justify-content: center; + + /* 基础阴影 */ + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); +} + +/* 2. 统一交互反馈:悬停增强阴影,移除缩放 */ +.btn-interect:hover, .btn-save:hover, .btn-cancel:hover { + filter: brightness(1.05); /* 悬停微亮 */ + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12); /* 悬停时投影加深,产生“浮起”感 */ +} + +/* 移除 active 缩放,改为颜色反馈 */ +.btn-interect:active, .btn-save:active, .btn-cancel:active { + filter: brightness(0.95); /* 按下稍微变暗 */ + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + transform: none; /* 明确去掉缩放 */ +} + +.btn-save, .btn-cancel { + flex: 1; +} + +/* 3. 颜色方案优化 */ +.btn-interect, .btn-save { + background: #1677ff; + color: white; + border: 1px solid #0958d9; /* 增加深色边框增强质感 */ +} + +.btn-cancel { + background: #ffffff; /* 改为白色底,配合阴影更好看 */ + color: #4b5563; + border: 1px solid #e5e7eb; /* 浅灰色边框 */ +} + +.btn-cancel:hover { + background: #f9fafb; + color: #1f2937; +} + +/* 补充:调整按钮组的间距,让阴影不被遮挡 */ +.editor-actions { + display: flex; + gap: 12px; + margin-top: 24px; + padding-top: 16px; + border-top: 1px solid #f3f4f6; +} + +/* 移除所有按钮默认的 outline */ +.btn-interect:focus, +.btn-save:focus, +.btn-cancel:focus, +.close-btn:focus { + outline: none; +} + +/* 蓝色按钮的焦点效果 (Save / Interect) */ +.btn-interect:focus-visible, +.btn-save:focus-visible { + box-shadow: 0 0 0 3px rgba(22, 119, 255, 0.3); /* 柔和的蓝色光晕 */ + border-color: #1677ff; +} + +/* 灰色按钮的焦点效果 (Cancel) */ +.btn-cancel:focus-visible { + box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.05); /* 极浅的灰色光晕 */ + border-color: #d1d5db; +} + +/* 关闭按钮的焦点效果 */ +.close-btn:focus-visible { + background-color: #f3f4f6; + border-radius: 4px; } \ No newline at end of file diff --git a/src/components/NodeEditor.tsx b/src/components/NodeEditor.tsx index ef75323..06b1284 100644 --- a/src/components/NodeEditor.tsx +++ b/src/components/NodeEditor.tsx @@ -1,7 +1,7 @@ import { useState, ReactNode } from 'react'; -import { NodeData, Settings } from '../types/graph'; +import { NodeData, Settings, NodeDataUpdate } from '../types/graph'; import { generateWireGuardPrivateKey } from '../utils/wireguardConfig' -import './NodeEditor.css'; +import './FormEditor.css'; import Folder from './Folder' @@ -10,15 +10,10 @@ interface Validation { errors: string[] } -function validateNodeConfig(formData : NodeData) : Validation { - // todo - return {isValid : true, errors: []} -} - interface NodeEditorProps { node: NodeData; settings: Settings; - onUpdate: (data: NodeData) => void; + onUpdate: (data: NodeDataUpdate) => void; onClose: () => void; } @@ -29,36 +24,48 @@ export default function NodeEditor({ onClose }: NodeEditorProps): ReactNode { - const [formData, setFormData] = useState(node); const [errors, setErrors] = useState([]); - const handleInputChange = (field: keyof NodeData, value: string): void => { - setFormData(prev => ({ - ...prev, - [field]: value - })); - }; + const [label, setLabel] = useState(node.label); + const [privateKey, setPrivateKey] = useState(node.privateKey); + const [ipv4Address, setIpv4Address] = useState(node.ipv4Address); + const [ipv6Address, setIpv6Address] = useState(node.ipv6Address); + const [disallowIPs, setDisallowIPs] = useState(node.disallowIPs); + const [listenPort, setListenPort] = useState(node.listenPort); + const [mtu, setmtu] = useState(node.mtu); + const [dnsServers, setdnsServers] = useState(node.dnsServers) + const [postUp, setPostUp] = useState(node.postUp) + const [postDown, setPostDown] = useState(node.postDown) + const [notes, setNotes] = useState(node.notes) const handleSave = (): void => { - const validation = validateNodeConfig(formData); - if (!validation.isValid) { - setErrors(validation.errors); - return; - } + // const validation = validateNodeConfig(formData); + // if (!validation.isValid) { + // setErrors(validation.errors); + // return; + // } setErrors([]); - onUpdate(formData); + onUpdate({ + label: label, + privateKey: privateKey, + ipv4Address: ipv4Address, + ipv6Address: ipv6Address, + disallowIPs: disallowIPs, + postUp: postUp, + postDown: postDown, + mtu: mtu, + listenPort: listenPort, + dnsServers: dnsServers, + notes: notes + }); onClose(); }; - const handleGenerateKey = (): void => { - handleInputChange('privateKey', generateWireGuardPrivateKey()) - } - return (
-

编辑节点: {formData.label || '新节点'}

+

编辑节点: {label}

@@ -80,8 +87,8 @@ export default function NodeEditor({ handleInputChange('label', e.target.value)} + value={label} + onChange={e => setLabel(e.target.value)} placeholder="例如: Node-A" />
@@ -90,10 +97,11 @@ export default function NodeEditor({
- +
@@ -102,8 +110,8 @@ export default function NodeEditor({ handleInputChange('ipv4Address', e.target.value)} + value={ipv4Address || ''} + onChange={e => setIpv4Address(e.target.value)} /> )} @@ -113,8 +121,8 @@ export default function NodeEditor({ handleInputChange('ipv6Address', e.target.value)} + value={ipv6Address || ''} + onChange={e => setIpv6Address(e.target.value)} /> )} @@ -124,8 +132,8 @@ export default function NodeEditor({ handleInputChange('disallowIPs', e.target.value)} + value={disallowIPs || ''} + onChange={e => setDisallowIPs(e.target.value)} /> @@ -136,8 +144,11 @@ export default function NodeEditor({ min="1024" max="49151" step="1" - value={formData.listenPort || ''} - onChange={(e) => handleInputChange('listenPort', e.target.value)} + value={listenPort || ''} + onChange={e => { + const value = e.target.valueAsNumber; + setListenPort(isNaN(value) ? undefined : value); + }} placeholder={`默认值:${settings.listenPort}`} /> @@ -148,8 +159,11 @@ export default function NodeEditor({ type="number" min="1" step="1" - value={formData.mtu || ''} - onChange={(e) => handleInputChange('mtu', e.target.value)} + value={mtu || ''} + onChange={e => { + const value = e.target.valueAsNumber; + setmtu(isNaN(value) ? undefined : value); + }} placeholder={settings.mtu ? `默认值:${settings.mtu}` : ''} /> @@ -158,8 +172,8 @@ export default function NodeEditor({ handleInputChange('dnsServers', e.target.value)} + value={dnsServers || ''} + onChange={(e) => setdnsServers(e.target.value)} placeholder="例如: 8.8.8.8,1.1.1.1" /> @@ -168,8 +182,8 @@ export default function NodeEditor({