From 6cf0af45289d7e1aa09b476310ad7c634fe56ddc Mon Sep 17 00:00:00 2001 From: limil Date: Sat, 7 Feb 2026 12:21:06 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E8=8A=82=E7=82=B9=E7=BC=96?= =?UTF-8?q?=E8=BE=91=E7=9A=84=E5=A4=96=E8=A7=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 38 ++----- src/components/Folder.css | 66 +++++++++++++ src/components/Folder.tsx | 27 +++++ src/components/NodeEditor.css | 104 ++++++++++++++++--- src/components/NodeEditor.tsx | 181 ++++++++++++++++++---------------- src/types/graph.ts | 31 +++--- 6 files changed, 307 insertions(+), 140 deletions(-) create mode 100644 src/components/Folder.css create mode 100644 src/components/Folder.tsx diff --git a/src/App.tsx b/src/App.tsx index 6132625..bb9c9dd 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -27,42 +27,20 @@ import './App.css'; const initialNodes: AppNode[] = []; const initialEdges: AppEdge[] = []; const initialSettings : Settings = { - v4SubNetPrefix: [172, 29], listenPort: 38894, + mtu: 1420, }; -export function generateEdgeId() : string { - return `e-${crypto.randomUUID()}`; -} - -export function generateNodeId() : string { - return `n-${crypto.randomUUID()}`; -} - const nodeTypes : NodeTypes = { custom: CustomNode, }; -function getFirstAvailableId(ids: number[]) : number { - const idSet = new Set(ids); - let id = 1; - while (idSet.has(id)) { - id++; - } - return id; -} - -function generateNodeData(nodes : NodeData[]) : NodeData | null { - const hostId = getFirstAvailableId(nodes.filter(n => n.groupId == 0).map(n => n.hostId)) - if(hostId > 255) return null - +function generateNodeData(count: number) : NodeData | null { const privateKey = generateWireGuardPrivateKey(); - const node : NodeData = { - label: `Node-${hostId}`, - privateKey: privateKey, - groupId: 0, - hostId: hostId, + id: `n-${crypto.randomUUID()}`, + label: `Node-${count + 1}`, + privateKey: privateKey } return node } @@ -87,7 +65,7 @@ function FlowContent(): ReactNode { (params) => { const newEdge : AppEdge = { ...params, - id: `e-${Date.now()}`, + id: `e-${crypto.randomUUID()}`, animated: !enableTwoWay, markerEnd: enableTwoWay ? undefined : { type: MarkerType.ArrowClosed }, data : { @@ -118,10 +96,10 @@ function FlowContent(): ReactNode { [edges]); const handleAddNode = (): void => { - const result = generateNodeData(nodes.map(n => n.data)); + const result = generateNodeData(nodes.length); if(result == null) return; const newNode: AppNode = { - id: generateNodeId(), + id: result.id, position: { x: 0, y: 0 }, data: result, type: 'custom', diff --git a/src/components/Folder.css b/src/components/Folder.css new file mode 100644 index 0000000..2c3f79c --- /dev/null +++ b/src/components/Folder.css @@ -0,0 +1,66 @@ +.folder-item { + border: 1px solid #e5e7eb; + border-radius: 8px; + margin-bottom: 8px; + background-color: #ffffff; + overflow: hidden; + transition: box-shadow 0.2s ease; +} + +/* 悬停微阴影 */ +.folder-item:hover { + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); +} + +.folder-header { + padding: 12px 16px; + display: flex; + justify-content: space-between; + align-items: center; + cursor: pointer; + background-color: #f9fafb; + font-weight: 500; + color: #374151; + + /* 核心修改:禁止选中标题文字 */ + user-select: none; + -webkit-user-select: none; +} + +.folder-header:hover { + background-color: #f3f4f6; +} + +/* 箭头旋转动画 */ +.arrow { + font-size: 12px; + transition: transform 0.3s ease; + color: #9ca3af; +} + +.show-arrow { + transform: rotate(180deg); +} + +/* 核心动画逻辑 */ +.folder-content { + display: grid; + grid-template-rows: 0fr; + transition: grid-template-rows 0.3s cubic-bezier(0.4, 0, 0.2, 1); + overflow: hidden; +} + +.folder-content.show { + grid-template-rows: 1fr; +} + +.content-inner { + min-height: 0; + padding: 0 16px; /* 默认隐藏时 padding 也是 0 */ + transition: padding 0.3s; +} + +.show .content-inner { + padding: 12px 16px; /* 展开后增加内边距 */ + border-top: 1px solid #f3f4f6; +} \ No newline at end of file diff --git a/src/components/Folder.tsx b/src/components/Folder.tsx new file mode 100644 index 0000000..f5178fc --- /dev/null +++ b/src/components/Folder.tsx @@ -0,0 +1,27 @@ +import { useState, ReactNode } from 'react'; +import './Folder.css'; + +interface FolderProps { + title: string; + children?: ReactNode; +} + +export default function Folder({ title, children }: FolderProps): ReactNode { + const [isOpen, setIsOpen] = useState(false); + + return ( +
+
setIsOpen(!isOpen)}> + {title} + {/* 使用同一个字符配合旋转动画,视觉更连贯 */} + +
+ + {children && ( +
+
{children}
+
+ )} +
+ ); +} \ No newline at end of file diff --git a/src/components/NodeEditor.css b/src/components/NodeEditor.css index c2d7e74..ee7eaa8 100644 --- a/src/components/NodeEditor.css +++ b/src/components/NodeEditor.css @@ -56,11 +56,44 @@ } .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 { @@ -118,41 +151,90 @@ gap: 10px; } - -/* 1. 基础通用样式 */ +/* 1. 基础通用样式 - 强化了投影和圆角 */ .btn-interect, .btn-save, .btn-cancel { - padding: 8px 16px; /* 补充了间距,确保按钮有厚度 */ + padding: 10px 20px; border: none; - border-radius: 4px; + border-radius: 6px; /* 稍微圆润一点,更现代 */ font-size: 14px; font-weight: 500; cursor: pointer; - transition: all 0.1s ease-in-out; + 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. 统一交互反馈:悬停变深,点下缩放 */ +/* 2. 统一交互反馈:悬停增强阴影,移除缩放 */ .btn-interect:hover, .btn-save:hover, .btn-cancel:hover { - filter: brightness(0.9); /* 自动变深,无需指定具体颜色 */ - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + 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 { - transform: scale(0.96); + 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: #f5f5f5; - color: #333; + 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 bda7a2f..0c4abc3 100644 --- a/src/components/NodeEditor.tsx +++ b/src/components/NodeEditor.tsx @@ -2,8 +2,8 @@ import { useState, ReactNode } from 'react'; import { NodeData, Settings } from '../types/graph'; import { generateWireGuardPrivateKey } from '../utils/wireguardConfig' import './NodeEditor.css'; - - +import Folder from './Folder' +import { randomBytes } from 'tweetnacl'; interface NodeEditorProps { node: NodeData; @@ -18,6 +18,7 @@ export default function NodeEditor({ onUpdate, onClose }: NodeEditorProps): ReactNode { + const [formData, setFormData] = useState(node); const [errors, setErrors] = useState([]); @@ -29,6 +30,7 @@ export default function NodeEditor({ }; const handleSave = (): void => { + setErrors(["123"]) // const validation = validateNodeConfig(formData); // if (!validation.isValid) { // setErrors(validation.errors); @@ -54,6 +56,12 @@ export default function NodeEditor({ {errors.length > 0 && (
+
setErrors([])} // 点击清空错误数组 + title="关闭提示" + >×
+ {errors.map((error, idx) => (

• {error}

))} @@ -81,95 +89,100 @@ export default function NodeEditor({
-
- - handleInputChange('hostId', e.target.value)} - placeholder="同一子网ID下主机ID不能重复,不得超过255" - /> -
+ {settings.ipv4Subnet && ( +
+ + handleInputChange('ipv4Address', e.target.value)} + /> +
+ )} -
- - handleInputChange('groupId', e.target.value)} - placeholder="不得超过255" - /> -
+ {settings.ipv6Subnet && ( +
+ + handleInputChange('ipv6Address', e.target.value)} + /> +
+ )} - {/* host options */} + +
+ + handleInputChange('disallowIPs', e.target.value)} + /> +
-
- - handleInputChange('postUp', e.target.value)} - /> -
+
+ + handleInputChange('listenPort', e.target.value)} + placeholder={`默认值:${settings.listenPort}`} + /> +
-
- - handleInputChange('postDown', e.target.value)} - /> -
+
+ + handleInputChange('mtu', e.target.value)} + placeholder={settings.mtu ? `默认值:${settings.mtu}` : ''} + /> +
-
- - handleInputChange('mtu', e.target.value)} - placeholder={settings.mtu ? `默认值:${settings.mtu}` : ''} - /> -
+
+ + handleInputChange('dnsServers', e.target.value)} + placeholder="例如: 8.8.8.8,1.1.1.1" + /> +
+ +
+ +