Compare commits

..

No commits in common. "a469133446ee93053ba0f312c16811c09f529507" and "0c5668838f63248f40b5fa32e56df54a3bc1480f" have entirely different histories.

7 changed files with 148 additions and 332 deletions

View File

@ -27,20 +27,42 @@ import './App.css';
const initialNodes: AppNode[] = []; const initialNodes: AppNode[] = [];
const initialEdges: AppEdge[] = []; const initialEdges: AppEdge[] = [];
const initialSettings : Settings = { const initialSettings : Settings = {
v4SubNetPrefix: [172, 29],
listenPort: 38894, listenPort: 38894,
mtu: 1420,
}; };
export function generateEdgeId() : string {
return `e-${crypto.randomUUID()}`;
}
export function generateNodeId() : string {
return `n-${crypto.randomUUID()}`;
}
const nodeTypes : NodeTypes = { const nodeTypes : NodeTypes = {
custom: CustomNode, custom: CustomNode,
}; };
function generateNodeData(count: number) : NodeData | null { 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
const privateKey = generateWireGuardPrivateKey(); const privateKey = generateWireGuardPrivateKey();
const node : NodeData = { const node : NodeData = {
id: `n-${crypto.randomUUID()}`, label: `Node-${hostId}`,
label: `Node-${count + 1}`, privateKey: privateKey,
privateKey: privateKey groupId: 0,
hostId: hostId,
} }
return node return node
} }
@ -65,7 +87,7 @@ function FlowContent(): ReactNode {
(params) => { (params) => {
const newEdge : AppEdge = { const newEdge : AppEdge = {
...params, ...params,
id: `e-${crypto.randomUUID()}`, id: `e-${Date.now()}`,
animated: !enableTwoWay, animated: !enableTwoWay,
markerEnd: enableTwoWay ? undefined : { type: MarkerType.ArrowClosed }, markerEnd: enableTwoWay ? undefined : { type: MarkerType.ArrowClosed },
data : { data : {
@ -96,10 +118,10 @@ function FlowContent(): ReactNode {
[edges]); [edges]);
const handleAddNode = (): void => { const handleAddNode = (): void => {
const result = generateNodeData(nodes.length); const result = generateNodeData(nodes.map(n => n.data));
if(result == null) return; if(result == null) return;
const newNode: AppNode = { const newNode: AppNode = {
id: result.id, id: generateNodeId(),
position: { x: 0, y: 0 }, position: { x: 0, y: 0 },
data: result, data: result,
type: 'custom', type: 'custom',
@ -111,7 +133,7 @@ function FlowContent(): ReactNode {
const handleUpdateNode = (updatedData: NodeData): void => { const handleUpdateNode = (updatedData: NodeData): void => {
setNodes((prev) => setNodes((prev) =>
prev.map((node) => { prev.map((node) => {
if (node.data.id === editingNode?.id) { if (node.data.label === editingNode?.label) {
return { ...node, data: updatedData }; return { ...node, data: updatedData };
} }
return node; return node;

View File

@ -1,9 +0,0 @@
## 产品基本功能实现待办事项
- [x] 完成节点的编辑窗口
- [ ] 完成边的编辑窗口
- [ ] 完成全局设置编辑窗口以及相关联动
- [ ] 实现配置生成逻辑,并验证有效
- [ ] 实现配置保存和加载功能
- [ ] 实现加密功能(完全加密和只加密私钥)
- [ ] 完成!奖励自己

View File

@ -1,66 +0,0 @@
.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;
}

View File

@ -1,27 +0,0 @@
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 (
<div className="folder-item">
<div className="folder-header" onClick={() => setIsOpen(!isOpen)}>
<span>{title}</span>
{/* 使用同一个字符配合旋转动画,视觉更连贯 */}
<span className={`arrow ${isOpen ? 'show-arrow' : ''}`}></span>
</div>
{children && (
<div className={`folder-content ${isOpen ? 'show' : ''}`}>
<div className="content-inner">{children}</div>
</div>
)}
</div>
);
}

View File

@ -56,44 +56,11 @@
} }
.error-box { .error-box {
position: relative;
background: #fee; background: #fee;
border: 1px solid #fcc; border: 1px solid #fcc;
border-radius: 4px; border-radius: 4px;
padding: 12px; padding: 12px;
margin-bottom: 15px; 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 { .error-message {
@ -151,90 +118,41 @@
gap: 10px; gap: 10px;
} }
/* 1. 基础通用样式 - 强化了投影和圆角 */
/* 1. 基础通用样式 */
.btn-interect, .btn-save, .btn-cancel { .btn-interect, .btn-save, .btn-cancel {
padding: 10px 20px; padding: 8px 16px; /* 补充了间距,确保按钮有厚度 */
border: none; border: none;
border-radius: 6px; /* 稍微圆润一点,更现代 */ border-radius: 4px;
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
cursor: pointer; cursor: pointer;
transition: all 0.2s ease; /* 稍微拉长过渡,显得更平滑 */ transition: all 0.1s ease-in-out;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: 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 { .btn-interect:hover, .btn-save:hover, .btn-cancel:hover {
filter: brightness(1.05); /* 悬停微亮 */ filter: brightness(0.9); /* 自动变深,无需指定具体颜色 */
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12); /* 悬停时投影加深,产生“浮起”感 */ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
} }
/* 移除 active 缩放,改为颜色反馈 */
.btn-interect:active, .btn-save:active, .btn-cancel:active { .btn-interect:active, .btn-save:active, .btn-cancel:active {
filter: brightness(0.95); /* 按下稍微变暗 */ transform: scale(0.96);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
transform: none; /* 明确去掉缩放 */
} }
.btn-save, .btn-cancel { .btn-save, .btn-cancel {
flex: 1; flex: 1;
} }
/* 3. 颜色方案优化 */
.btn-interect, .btn-save { .btn-interect, .btn-save {
background: #1677ff; background: #1677ff;
color: white; color: white;
border: 1px solid #0958d9; /* 增加深色边框增强质感 */
} }
.btn-cancel { .btn-cancel {
background: #ffffff; /* 改为白色底,配合阴影更好看 */ background: #f5f5f5;
color: #4b5563; color: #333;
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;
} }

View File

@ -2,18 +2,8 @@ import { useState, ReactNode } from 'react';
import { NodeData, Settings } from '../types/graph'; import { NodeData, Settings } from '../types/graph';
import { generateWireGuardPrivateKey } from '../utils/wireguardConfig' import { generateWireGuardPrivateKey } from '../utils/wireguardConfig'
import './NodeEditor.css'; import './NodeEditor.css';
import Folder from './Folder'
interface Validation {
isValid: boolean,
errors: string[]
}
function validateNodeConfig(formData : NodeData) : Validation {
// todo
return {isValid : true, errors: []}
}
interface NodeEditorProps { interface NodeEditorProps {
node: NodeData; node: NodeData;
@ -28,7 +18,6 @@ export default function NodeEditor({
onUpdate, onUpdate,
onClose onClose
}: NodeEditorProps): ReactNode { }: NodeEditorProps): ReactNode {
const [formData, setFormData] = useState<NodeData>(node); const [formData, setFormData] = useState<NodeData>(node);
const [errors, setErrors] = useState<string[]>([]); const [errors, setErrors] = useState<string[]>([]);
@ -40,14 +29,15 @@ export default function NodeEditor({
}; };
const handleSave = (): void => { const handleSave = (): void => {
const validation = validateNodeConfig(formData); // const validation = validateNodeConfig(formData);
if (!validation.isValid) { // if (!validation.isValid) {
setErrors(validation.errors); // setErrors(validation.errors);
return; // return;
} // }
setErrors([]);
onUpdate(formData); // setErrors([]);
onClose(); // onUpdate(formData);
// onClose();
}; };
const handleGenerateKey = (): void => { const handleGenerateKey = (): void => {
@ -64,12 +54,6 @@ export default function NodeEditor({
{errors.length > 0 && ( {errors.length > 0 && (
<div className="error-box"> <div className="error-box">
<div
className="error-close-btn"
onClick={() => setErrors([])} // 点击清空错误数组
title="关闭提示"
>×</div>
{errors.map((error, idx) => ( {errors.map((error, idx) => (
<p key={idx} className="error-message"> {error}</p> <p key={idx} className="error-message"> {error}</p>
))} ))}
@ -97,100 +81,95 @@ export default function NodeEditor({
</div> </div>
</div> </div>
{settings.ipv4Subnet && ( <div className="form-group">
<div className="form-group"> <label>ID</label>
<label>IPv4地址</label> <input
<input type="number"
type="text" min="0"
value={formData.ipv4Address || ''} max="255"
onChange={(e) => handleInputChange('ipv4Address', e.target.value)} step="1"
/> value={formData.hostId}
</div> onChange={(e) => handleInputChange('hostId', e.target.value)}
)} placeholder="同一子网ID下主机ID不能重复不得超过255"
/>
</div>
{settings.ipv6Subnet && ( <div className="form-group">
<div className="form-group"> <label>ID</label>
<label>IPv6地址</label> <input
<input type="number"
type="text" min="0"
value={formData.ipv6Address || ''} max="255"
onChange={(e) => handleInputChange('ipv6Address', e.target.value)} step="1"
/> value={formData.groupId}
</div> onChange={(e) => handleInputChange('groupId', e.target.value)}
)} placeholder="不得超过255"
/>
</div>
<Folder title='高级'> {/* host options */}
<div className="form-group">
<label></label>
<input
type="text"
value={formData.disallowIPs || ''}
onChange={(e) => handleInputChange('disallowIPs', e.target.value)}
/>
</div>
<div className="form-group"> <div className="form-group">
<label></label> <label>PostUp ()</label>
<input <input
type="number" type="text"
min="1024" value={formData.postUp || ''}
max="49151" onChange={(e) => handleInputChange('postUp', e.target.value)}
step="1" />
value={formData.listenPort || ''} </div>
onChange={(e) => handleInputChange('listenPort', e.target.value)}
placeholder={`默认值:${settings.listenPort}`}
/>
</div>
<div className="form-group"> <div className="form-group">
<label>mtu</label> <label>PostDown ()</label>
<input <input
type="number" type="text"
min="1" value={formData.postDown || ''}
step="1" onChange={(e) => handleInputChange('postDown', e.target.value)}
value={formData.mtu || ''} />
onChange={(e) => handleInputChange('mtu', e.target.value)} </div>
placeholder={settings.mtu ? `默认值:${settings.mtu}` : ''}
/>
</div>
<div className="form-group"> <div className="form-group">
<label>DNS服务器</label> <label>mtu ()</label>
<input <input
type="text" type="number"
value={formData.dnsServers || ''} min="1"
onChange={(e) => handleInputChange('dnsServers', e.target.value)} step="1"
placeholder="例如: 8.8.8.8,1.1.1.1" value={formData.mtu || ''}
/> onChange={(e) => handleInputChange('mtu', e.target.value)}
</div> placeholder={settings.mtu ? `默认值:${settings.mtu}` : ''}
/>
</div>
<div className="form-group"> <div className="form-group">
<label>PostUp</label> <label> ()</label>
<textarea <input
rows={2} type="number"
value={formData.postUp || ''} min="1024"
onChange={(e) => handleInputChange('postUp', e.target.value)} max="49151"
/> step="1"
</div> value={formData.listenPort || ''}
onChange={(e) => handleInputChange('listenPort', e.target.value)}
placeholder={`默认值:${settings.listenPort}`}
/>
</div>
<div className="form-group"> <div className="form-group">
<label>PostDown</label> <label>DNS服务器 ()</label>
<textarea <input
rows={2} type="text"
value={formData.postDown || ''} value={formData.dnsServers || ''}
onChange={(e) => handleInputChange('postDown', e.target.value)} onChange={(e) => handleInputChange('dnsServers', e.target.value)}
/> placeholder="例如: 8.8.8.8,1.1.1.1"
</div> />
</div>
<div className="form-group"> <div className="form-group">
<label></label> <label> ()</label>
<textarea <textarea
rows={4} rows={3}
value={formData.notes || ''} value={formData.notes || ''}
onChange={(e) => handleInputChange('notes', e.target.value)} onChange={(e) => handleInputChange('notes', e.target.value)}
/> />
</div> </div>
</Folder>
<div className="editor-actions"> <div className="editor-actions">
<button className="btn-save" onClick={handleSave}></button> <button className="btn-save" onClick={handleSave}></button>

View File

@ -4,8 +4,16 @@ export type AppNode = Node<NodeData>;
export type AppEdge = Edge<EdgeData>; export type AppEdge = Edge<EdgeData>;
export class SubNetRouter {
private _nodes : Record<string, string | undefined> = {};
constructor(
public subnet: string,
) {}
}
export type NodeData = { export type NodeData = {
readonly id: string; id: string;
label: string; label: string;
privateKey: string; privateKey: string;
ipv4Address?: string; ipv4Address?: string;
@ -20,27 +28,18 @@ export type NodeData = {
notes?: string; notes?: string;
} }
export type EdgeData = {
isTwoWayEdge: boolean;
persistentKeepalive?: string;
}
export class SubNetRouter {
private _nodes : Record<string, string | undefined> = {};
constructor(
public subnet: string,
public readonly kind: 'ipv4' | 'ipv6'
) {}
}
export type Settings = { export type Settings = {
listenPort: number; listenPort: number;
mtu: number; mtu: number;
ipv4Subnet?: string; ipv4Subnet?: string;
ipv4SubNetRouter?: SubNetRouter; ipv4SubnetRouter?: SubNetRouter;
ipv6Subnet?: string; ipv6Subnet?: string;
ipv6SubnetRouter?: SubNetRouter; ipv6SubnetRouter?: SubNetRouter;
} }
export type EdgeData = {
isTwoWayEdge: boolean;
persistentKeepalive?: string;
}