优化交互界面
This commit is contained in:
parent
88fa8d3ad0
commit
855d0c8c28
17
src/App.css
17
src/App.css
@ -92,3 +92,20 @@
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.toolbar-right{
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.action-section{
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
gap:8px;
|
||||
padding:8px 12px;
|
||||
}
|
||||
.section-title{
|
||||
font-size:12px;
|
||||
color:#666;
|
||||
font-weight:600;
|
||||
margin-bottom:4px;
|
||||
}
|
||||
81
src/App.tsx
81
src/App.tsx
@ -31,6 +31,7 @@ import './App.css';
|
||||
import toast, { Toaster } from 'react-hot-toast';
|
||||
import SaveConfig from './types/saveConfig'
|
||||
import { plainToInstance, instanceToPlain } from 'class-transformer';
|
||||
import SaveLoadPanel from './components/SaveLoadPanel';
|
||||
|
||||
|
||||
const initialNodes: AppNode[] = [];
|
||||
@ -147,16 +148,15 @@ function FlowContent(): ReactNode {
|
||||
);
|
||||
};
|
||||
|
||||
const handleSaveConfig = async (): Promise<void> => {
|
||||
const handleSaveConfig = async (pass?: string): Promise<void> => {
|
||||
try {
|
||||
const pass = window.prompt('输入用于加密私钥的密码(留空则不加密):');
|
||||
const saveConfig: SaveConfig = {settings: settings, nodes: [], edges: [], encrypted: false}
|
||||
if(pass) saveConfig.encrypted = true;
|
||||
|
||||
nodes.forEach(node => {
|
||||
const nodeData = node.data;
|
||||
let privateKey = nodeData.privateKey;
|
||||
if(pass) privateKey = CryptoJS.AES.encrypt(privateKey, pass).toString();
|
||||
if(pass && typeof privateKey === 'string') privateKey = CryptoJS.AES.encrypt(privateKey, pass).toString();
|
||||
saveConfig.nodes.push({...node, data: {...nodeData, privateKey: privateKey}});
|
||||
});
|
||||
|
||||
@ -177,35 +177,25 @@ function FlowContent(): ReactNode {
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoadConfig = async (): Promise<void> => {
|
||||
const handleLoadConfig = async (fileText: string, pass?: string): Promise<void> => {
|
||||
try {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = 'application/json';
|
||||
input.onchange = async () => {
|
||||
const file = input.files?.[0];
|
||||
if (!file) return;
|
||||
const jsonString = await file.text();
|
||||
const plainObject = JSON.parse(jsonString);
|
||||
const saveConfig: SaveConfig = plainToInstance(SaveConfig, plainObject);
|
||||
const plainObject = JSON.parse(fileText);
|
||||
const saveConfig: SaveConfig = plainToInstance(SaveConfig, plainObject);
|
||||
|
||||
if (saveConfig.encrypted) {
|
||||
const pass = window.prompt('输入用于解密私钥的密码:');
|
||||
if (!pass) { toast.error('需要密码以解密私钥'); return; }
|
||||
try {
|
||||
for(let node of saveConfig.nodes) {
|
||||
node.data.privateKey = CryptoJS.AES.decrypt(node.data.privateKey, pass).toString(CryptoJS.enc.Utf8);
|
||||
}
|
||||
} catch (err) {
|
||||
toast.error('解密失败: ' + err);
|
||||
return;
|
||||
if (saveConfig.encrypted) {
|
||||
if (!pass) { toast.error('需要密码以解密私钥'); return; }
|
||||
try {
|
||||
for(let node of saveConfig.nodes) {
|
||||
node.data.privateKey = CryptoJS.AES.decrypt(node.data.privateKey, pass).toString(CryptoJS.enc.Utf8);
|
||||
}
|
||||
} catch (err) {
|
||||
toast.error('解密失败: ' + err);
|
||||
return;
|
||||
}
|
||||
setSettings(saveConfig.settings);
|
||||
setNodes(saveConfig.nodes);
|
||||
setEdges(saveConfig.edges);
|
||||
};
|
||||
input.click();
|
||||
}
|
||||
setSettings(saveConfig.settings);
|
||||
setNodes(saveConfig.nodes);
|
||||
setEdges(saveConfig.edges);
|
||||
} catch (e) {
|
||||
toast.error('加载失败: ' + e);
|
||||
}
|
||||
@ -243,27 +233,26 @@ function FlowContent(): ReactNode {
|
||||
|
||||
<div className="toolbar">
|
||||
<div className="toolbar-group">
|
||||
<Toggle
|
||||
className="toolbar-item"
|
||||
checked = {enableTwoWay}
|
||||
onChange={checked => setEnableTwoWay(checked)} >双向连接</Toggle>
|
||||
<div className="action-section">
|
||||
<div className="section-title">文件操作</div>
|
||||
<SaveLoadPanel onSave={handleSaveConfig} onLoad={handleLoadConfig} />
|
||||
</div>
|
||||
|
||||
<button className="toolbar-btn" onClick={handleAddNode} title="添加新节点">
|
||||
➕ 添加节点
|
||||
</button>
|
||||
<div className="action-section">
|
||||
<div className="section-title">图表操作</div>
|
||||
<Toggle
|
||||
className="toolbar-item"
|
||||
checked = {enableTwoWay}
|
||||
onChange={checked => setEnableTwoWay(checked)} >双向连接</Toggle>
|
||||
|
||||
<button className="toolbar-btn" onClick={() => {setEditSettings(true);}} title="设置">
|
||||
📋 设置
|
||||
</button>
|
||||
|
||||
<button className="toolbar-btn" onClick={handleSaveConfig} title="保存配置">
|
||||
💾 保存配置
|
||||
</button>
|
||||
|
||||
<button className="toolbar-btn" onClick={handleLoadConfig} title="加载配置">
|
||||
📂 加载配置
|
||||
</button>
|
||||
<button className="toolbar-btn" onClick={handleAddNode} title="添加新节点">
|
||||
➕ 添加节点
|
||||
</button>
|
||||
|
||||
<button className="toolbar-btn" onClick={() => {setEditSettings(true);}} title="设置">
|
||||
📋 设置
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
70
src/components/SaveLoadPanel.css
Normal file
70
src/components/SaveLoadPanel.css
Normal file
@ -0,0 +1,70 @@
|
||||
.save-load-panel{
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
align-items:stretch;
|
||||
gap:8px;
|
||||
padding:8px 0;
|
||||
width:auto;
|
||||
}
|
||||
.panel-section{
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
gap:6px;
|
||||
padding:8px;
|
||||
border:1px solid rgba(0,0,0,0.08);
|
||||
border-radius:6px;
|
||||
background: rgba(255,255,255,0.02);
|
||||
}
|
||||
.panel-title{
|
||||
font-size:12px;
|
||||
color:#666;
|
||||
}
|
||||
.panel-input{
|
||||
padding:6px 8px;
|
||||
font-size:13px;
|
||||
border:1px solid #ddd;
|
||||
border-radius:4px;
|
||||
}
|
||||
/* Use .toolbar-btn for button styles so Save/Load matches other toolbar actions */
|
||||
.save-load-panel .toolbar-btn{width:100%;text-align:left;padding:8px 16px;border-radius:4px}
|
||||
/* divider removed to match toolbar style */
|
||||
|
||||
/* Modal styles */
|
||||
.sl-modal{
|
||||
position:fixed;
|
||||
inset:0;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
z-index:200;
|
||||
}
|
||||
.sl-modal-backdrop{
|
||||
position:absolute;
|
||||
inset:0;
|
||||
background:rgba(0,0,0,0.4);
|
||||
}
|
||||
.sl-modal-content{
|
||||
position:relative;
|
||||
width:360px;
|
||||
background:#fff;
|
||||
border-radius:8px;
|
||||
box-shadow:0 8px 24px rgba(0,0,0,0.2);
|
||||
z-index:210;
|
||||
overflow:hidden;
|
||||
}
|
||||
.sl-modal-header{
|
||||
display:flex;
|
||||
justify-content:space-between;
|
||||
align-items:center;
|
||||
padding:12px 16px;
|
||||
border-bottom:1px solid #eee;
|
||||
}
|
||||
.sl-modal-title{font-weight:600}
|
||||
.sl-close{background:none;border:none;cursor:pointer;font-size:16px}
|
||||
.sl-modal-body{padding:12px 16px;display:flex;flex-direction:column;gap:8px}
|
||||
.sl-label{font-size:12px;color:#555}
|
||||
.sl-input{padding:8px;border:1px solid #ddd;border-radius:4px}
|
||||
.sl-file-input{padding:6px}
|
||||
.sl-hint{font-size:12px;color:#777;margin-top:6px}
|
||||
.sl-actions{display:flex;gap:8px;justify-content:flex-end}
|
||||
|
||||
97
src/components/SaveLoadPanel.tsx
Normal file
97
src/components/SaveLoadPanel.tsx
Normal file
@ -0,0 +1,97 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import './SaveLoadPanel.css';
|
||||
|
||||
interface Props {
|
||||
onSave: (pass?: string) => Promise<void>;
|
||||
onLoad: (fileText: string, pass?: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export default function SaveLoadPanel({ onSave, onLoad }: Props) {
|
||||
const [modalType, setModalType] = useState<'save' | 'load' | null>(null);
|
||||
const [password, setPassword] = useState('');
|
||||
const [pendingFileText, setPendingFileText] = useState<string | null>(null);
|
||||
|
||||
const openSave = () => {
|
||||
setPassword('');
|
||||
setModalType('save');
|
||||
};
|
||||
const openLoad = async () => {
|
||||
// immediately open file selector
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = 'application/json';
|
||||
input.onchange = async () => {
|
||||
const file = input.files?.[0];
|
||||
if (!file) return;
|
||||
const text = await file.text();
|
||||
try {
|
||||
const obj = JSON.parse(text);
|
||||
if (obj && obj.encrypted) {
|
||||
// need password to decrypt
|
||||
setPendingFileText(text);
|
||||
setPassword('');
|
||||
setModalType('load');
|
||||
} else {
|
||||
await onLoad(text, undefined);
|
||||
}
|
||||
} catch (err) {
|
||||
// if invalid json, still try to load
|
||||
await onLoad(text, undefined);
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
setModalType(null);
|
||||
setPassword('');
|
||||
};
|
||||
|
||||
const confirmSave = async () => {
|
||||
await onSave(password || undefined);
|
||||
closeModal();
|
||||
};
|
||||
|
||||
const confirmLoad = async () => {
|
||||
if (!pendingFileText) return;
|
||||
await onLoad(pendingFileText, password || undefined);
|
||||
setPendingFileText(null);
|
||||
closeModal();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="save-load-panel">
|
||||
<button className="toolbar-btn" onClick={openSave}>💾 保存</button>
|
||||
<button className="toolbar-btn" onClick={openLoad}>📂 加载</button>
|
||||
|
||||
{modalType && (
|
||||
<div className="sl-modal">
|
||||
<div className="sl-modal-backdrop" onClick={closeModal} />
|
||||
<div className="sl-modal-content">
|
||||
<div className="sl-modal-header">
|
||||
<div className="sl-modal-title">{modalType === 'save' ? '保存配置' : '加载配置'}</div>
|
||||
<button className="sl-close" onClick={closeModal}>✕</button>
|
||||
</div>
|
||||
<div className="sl-modal-body">
|
||||
<label className="sl-label">加密密码(可选)</label>
|
||||
<input type="password" className="sl-input" value={password} onChange={e => setPassword(e.target.value)} />
|
||||
|
||||
{modalType === 'load' ? (
|
||||
<div className="sl-load-row">
|
||||
<div className="sl-hint">检测到加密文件,请输入密码后点击确认加载</div>
|
||||
<div className="sl-actions">
|
||||
<button className="toolbar-btn" onClick={confirmLoad}>确认加载</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="sl-actions">
|
||||
<button className="toolbar-btn" onClick={confirmSave}>确认保存</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user