Commit 304fdc2c by wanjia

迭代一版

parent b5b01eb1
...@@ -3,13 +3,13 @@ const { contextBridge, ipcRenderer } = require('electron'); ...@@ -3,13 +3,13 @@ const { contextBridge, ipcRenderer } = require('electron');
// 暴露安全的 API 到渲染进程 // 暴露安全的 API 到渲染进程
contextBridge.exposeInMainWorld('electronAPI', { contextBridge.exposeInMainWorld('electronAPI', {
// 文件系统操作 // 文件系统操作
getGroups: () => ipcRenderer.invoke('get-groups'),
createGroup: (groupName) => ipcRenderer.invoke('create-group', groupName), createGroup: (groupName) => ipcRenderer.invoke('create-group', groupName),
deleteGroup: (groupId) => ipcRenderer.invoke('delete-group', groupId), deleteGroup: (groupId) => ipcRenderer.invoke('delete-group', groupId),
createFile: (groupId, fileName) => ipcRenderer.invoke('create-file', groupId, fileName), createFile: (groupId, fileName) => ipcRenderer.invoke('create-file', groupId, fileName),
deleteFile: (groupId, fileId) => ipcRenderer.invoke('delete-file', groupId, fileId), deleteFile: (groupId, fileId) => ipcRenderer.invoke('delete-file', groupId, fileId),
saveFile: (fileId, content) => ipcRenderer.invoke('save-file', fileId, content), loadFile: (fileId) => ipcRenderer.invoke('load-file-content', fileId),
loadFile: (fileId) => ipcRenderer.invoke('load-file', fileId), saveFile: (fileId, content) => ipcRenderer.invoke('save-file-content', fileId, content),
getGroups: () => ipcRenderer.invoke('get-groups'),
// 系统信息 // 系统信息
platform: process.platform platform: process.platform
......
...@@ -27,11 +27,19 @@ import './components/Sidebar.css'; ...@@ -27,11 +27,19 @@ import './components/Sidebar.css';
type ViewMode = 'split' | 'edit' | 'preview'; type ViewMode = 'split' | 'edit' | 'preview';
interface Document {
id: string;
groupId: string;
name: string;
content: string;
}
const App: React.FC = () => { const App: React.FC = () => {
const editorRef = useRef<any>(null); const editorRef = useRef<any>(null);
const [markdown, setMarkdown] = useState<string>(''); const [markdown, setMarkdown] = useState<string>('');
const [viewMode, setViewMode] = useState<ViewMode>('split'); const [viewMode, setViewMode] = useState<ViewMode>('split');
const [isSidebarOpen, setIsSidebarOpen] = useState(true); const [isSidebarOpen, setIsSidebarOpen] = useState(true);
const [currentDocument, setCurrentDocument] = useState<Document | null>(null);
const handleEditorDidMount = (editor: any) => { const handleEditorDidMount = (editor: any) => {
editorRef.current = editor; editorRef.current = editor;
...@@ -39,6 +47,9 @@ const App: React.FC = () => { ...@@ -39,6 +47,9 @@ const App: React.FC = () => {
const handleEditorChange = (value: string | undefined) => { const handleEditorChange = (value: string | undefined) => {
setMarkdown(value || ''); setMarkdown(value || '');
if (currentDocument) {
window.electronAPI.saveFile(currentDocument.id, value || '');
}
}; };
const insertAtCursor = (before: string, after: string = '') => { const insertAtCursor = (before: string, after: string = '') => {
...@@ -110,13 +121,33 @@ const App: React.FC = () => { ...@@ -110,13 +121,33 @@ const App: React.FC = () => {
} }
}, []); }, []);
const handleDocumentSelect = async (groupId: string, fileId: string) => {
try {
const content = await window.electronAPI.loadFile(fileId);
setCurrentDocument({
id: fileId,
groupId,
name: '', // 这个值会从 Sidebar 组件传过来
content
});
setMarkdown(content);
} catch (error) {
console.error('Failed to load document:', error);
}
};
const toggleSidebar = () => { const toggleSidebar = () => {
setIsSidebarOpen(!isSidebarOpen); setIsSidebarOpen(!isSidebarOpen);
}; };
return ( return (
<div className="app"> <div className="app">
<Sidebar isOpen={isSidebarOpen} onToggle={() => setIsSidebarOpen(!isSidebarOpen)} /> <Sidebar
isOpen={isSidebarOpen}
onToggle={() => setIsSidebarOpen(!isSidebarOpen)}
onDocumentSelect={handleDocumentSelect}
currentDocumentId={currentDocument?.id}
/>
<div className="main-content"> <div className="main-content">
<div className="toolbar"> <div className="toolbar">
<div className="toolbar-group"> <div className="toolbar-group">
......
.confirm-dialog-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.confirm-dialog {
background-color: #fff;
border-radius: 8px;
padding: 24px;
width: 400px;
max-width: 90%;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.confirm-dialog-title {
margin: 0 0 16px 0;
font-size: 1.25rem;
color: #333;
}
.confirm-dialog-message {
margin: 0 0 24px 0;
color: #666;
line-height: 1.5;
}
.confirm-dialog-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
}
.confirm-dialog-button {
padding: 8px 16px;
border: none;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: background-color 0.2s;
}
.confirm-dialog-button.cancel {
background-color: #f5f5f5;
color: #333;
}
.confirm-dialog-button.cancel:hover {
background-color: #e8e8e8;
}
.confirm-dialog-button.confirm {
background-color: #dc3545;
color: white;
}
.confirm-dialog-button.confirm:hover {
background-color: #c82333;
}
import React from 'react';
import './ConfirmDialog.css';
interface ConfirmDialogProps {
isOpen: boolean;
title: string;
message: string;
onConfirm: () => void;
onCancel: () => void;
}
const ConfirmDialog: React.FC<ConfirmDialogProps> = ({
isOpen,
title,
message,
onConfirm,
onCancel,
}) => {
if (!isOpen) return null;
return (
<div className="confirm-dialog-overlay">
<div className="confirm-dialog">
<h3 className="confirm-dialog-title">{title}</h3>
<p className="confirm-dialog-message">{message}</p>
<div className="confirm-dialog-actions">
<button className="confirm-dialog-button cancel" onClick={onCancel}>
Cancel
</button>
<button className="confirm-dialog-button confirm" onClick={onConfirm}>
Confirm
</button>
</div>
</div>
</div>
);
};
export default ConfirmDialog;
...@@ -61,44 +61,67 @@ ...@@ -61,44 +61,67 @@
} }
.group { .group {
margin-bottom: 10px; margin: 8px 0;
} }
.group-header { .group-header {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 6px 8px; padding: 8px 12px;
background-color: #e8e8e8; cursor: pointer;
border-radius: 4px; border-radius: 4px;
margin-bottom: 4px; transition: background-color 0.2s;
}
.group-header:hover {
background-color: rgba(255, 255, 255, 0.1);
} }
.group-name { .group-name {
font-weight: 500;
color: #333;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 6px; gap: 8px;
font-weight: 500;
} }
.group-actions { .group-toggle-icon {
display: flex; width: 12px;
gap: 4px; height: 12px;
transition: transform 0.2s;
}
.group-files {
margin-left: 16px;
transition: max-height 0.3s ease-out, opacity 0.2s ease-out;
max-height: 1000px;
opacity: 1;
overflow: hidden;
}
.group-files.collapsed {
max-height: 0;
opacity: 0;
} }
.file-item { .file-item {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 4px 8px; padding: 8px 12px;
margin-left: 8px; margin: 2px 0;
border-radius: 4px;
cursor: pointer; cursor: pointer;
border-radius: 4px;
transition: background-color 0.2s;
} }
.file-item:hover { .file-item:hover {
background-color: #e8e8e8; background-color: rgba(255, 255, 255, 0.1);
}
.file-item.active {
background-color: rgba(255, 255, 255, 0.15);
font-weight: 500;
} }
.file-name { .file-name {
......
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { import {
faChevronLeft, faChevronLeft,
faChevronRight, faChevronRight,
faChevronDown,
faFolder, faFolder,
faFolderPlus, faFolderPlus,
faFile, faFile,
faPlus, faPlus,
faTrash faTrash
} from '@fortawesome/free-solid-svg-icons'; } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import './Sidebar.css'; import './Sidebar.css';
import ConfirmDialog from './ConfirmDialog';
interface SidebarProps { interface SidebarProps {
isOpen: boolean; isOpen: boolean;
onToggle: () => void; onToggle: () => void;
onDocumentSelect: (groupId: string, fileId: string) => void;
currentDocumentId: string | undefined;
} }
interface Group { interface Group {
...@@ -28,12 +32,25 @@ interface File { ...@@ -28,12 +32,25 @@ interface File {
content: string; content: string;
} }
const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle }) => { const Sidebar: React.FC<SidebarProps> = ({
isOpen,
onToggle,
onDocumentSelect,
currentDocumentId
}) => {
const [groups, setGroups] = useState<Group[]>([]); const [groups, setGroups] = useState<Group[]>([]);
const [showNewGroupForm, setShowNewGroupForm] = useState(false); const [showNewGroupForm, setShowNewGroupForm] = useState(false);
const [newGroupName, setNewGroupName] = useState(''); const [newGroupName, setNewGroupName] = useState('');
const [showNewFileForm, setShowNewFileForm] = useState<string | null>(null); const [showNewFileForm, setShowNewFileForm] = useState<string | null>(null);
const [newFileName, setNewFileName] = useState(''); const [newFileName, setNewFileName] = useState('');
const [collapsedGroups, setCollapsedGroups] = useState<Set<string>>(new Set());
const [deleteConfirm, setDeleteConfirm] = useState<{
type: 'group' | 'file';
groupId: string;
fileId?: string;
name: string;
isOpen: boolean;
} | null>(null);
useEffect(() => { useEffect(() => {
// 从后端加载组和文件 // 从后端加载组和文件
...@@ -55,12 +72,15 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle }) => { ...@@ -55,12 +72,15 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle }) => {
}; };
const handleDeleteGroup = async (groupId: string) => { const handleDeleteGroup = async (groupId: string) => {
try { const group = groups.find(g => g.id === groupId);
await window.electronAPI.deleteGroup(groupId); if (!group) return;
setGroups(groups.filter(group => group.id !== groupId));
} catch (error) { setDeleteConfirm({
console.error('Failed to delete group:', error); type: 'group',
} groupId,
name: group.name,
isOpen: true
});
}; };
const handleCreateFile = async (groupId: string) => { const handleCreateFile = async (groupId: string) => {
...@@ -78,26 +98,64 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle }) => { ...@@ -78,26 +98,64 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle }) => {
})); }));
setNewFileName(''); setNewFileName('');
setShowNewFileForm(null); setShowNewFileForm(null);
// 创建后自动选择新文件
onDocumentSelect(groupId, newFile.id);
} catch (error) { } catch (error) {
console.error('Failed to create file:', error); console.error('Failed to create file:', error);
} }
}; };
const handleDeleteFile = async (groupId: string, fileId: string) => { const handleDeleteFile = async (groupId: string, fileId: string) => {
const group = groups.find(g => g.id === groupId);
const file = group?.files.find(f => f.id === fileId);
if (!group || !file) return;
setDeleteConfirm({
type: 'file',
groupId,
fileId,
name: file.name,
isOpen: true
});
};
const confirmDelete = async () => {
if (!deleteConfirm) return;
try { try {
await window.electronAPI.deleteFile(groupId, fileId); if (deleteConfirm.type === 'group') {
setGroups(groups.map(group => { await window.electronAPI.deleteGroup(deleteConfirm.groupId);
if (group.id === groupId) { setGroups(groups.filter(g => g.id !== deleteConfirm.groupId));
return { } else {
...group, if (!deleteConfirm.fileId) return;
files: group.files.filter(file => file.id !== fileId) await window.electronAPI.deleteFile(deleteConfirm.groupId, deleteConfirm.fileId);
}; setGroups(groups.map(group => {
} if (group.id === deleteConfirm.groupId) {
return group; return {
})); ...group,
} catch (error) { files: group.files.filter(f => f.id !== deleteConfirm.fileId)
console.error('Failed to delete file:', error); };
}
return group;
}));
}
} catch (err) {
console.error(`Failed to delete ${deleteConfirm.type}:`, err);
} }
setDeleteConfirm(null);
};
const toggleGroup = (groupId: string) => {
setCollapsedGroups(prev => {
const newSet = new Set(prev);
if (newSet.has(groupId)) {
newSet.delete(groupId);
} else {
newSet.add(groupId);
}
return newSet;
});
}; };
return ( return (
...@@ -122,15 +180,31 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle }) => { ...@@ -122,15 +180,31 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle }) => {
{groups.map(group => ( {groups.map(group => (
<div key={group.id} className="group"> <div key={group.id} className="group">
<div className="group-header"> <div className="group-header" onClick={() => toggleGroup(group.id)}>
<span className="group-name"> <span className="group-name">
<FontAwesomeIcon
icon={collapsedGroups.has(group.id) ? faChevronRight : faChevronDown}
className="group-toggle-icon"
/>
<FontAwesomeIcon icon={faFolder} /> {group.name} <FontAwesomeIcon icon={faFolder} /> {group.name}
</span> </span>
<div className="group-actions"> <div className="group-actions">
<button className="action-button" onClick={() => setShowNewFileForm(group.id)}> <button
className="action-button"
onClick={(e) => {
e.stopPropagation();
setShowNewFileForm(group.id);
}}
>
<FontAwesomeIcon icon={faPlus} /> <FontAwesomeIcon icon={faPlus} />
</button> </button>
<button className="action-button" onClick={() => handleDeleteGroup(group.id)}> <button
className="action-button"
onClick={(e) => {
e.stopPropagation();
handleDeleteGroup(group.id);
}}
>
<FontAwesomeIcon icon={faTrash} /> <FontAwesomeIcon icon={faTrash} />
</button> </button>
</div> </div>
...@@ -149,27 +223,44 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle }) => { ...@@ -149,27 +223,44 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle }) => {
</div> </div>
)} )}
{group.files.map(file => ( <div className={`group-files ${collapsedGroups.has(group.id) ? 'collapsed' : ''}`}>
<div key={file.id} className="file-item"> {group.files.map(file => (
<span className="file-name"> <div
<FontAwesomeIcon icon={faFile} /> {file.name} key={file.id}
</span> className={`file-item ${currentDocumentId === file.id ? 'active' : ''}`}
<div className="file-actions"> onClick={() => onDocumentSelect(group.id, file.id)}
<button >
className="action-button" <span className="file-name">
onClick={() => handleDeleteFile(group.id, file.id)} <FontAwesomeIcon icon={faFile} /> {file.name}
> </span>
<FontAwesomeIcon icon={faTrash} /> <div className="file-actions">
</button> <button
className="action-button"
onClick={(e) => {
e.stopPropagation();
handleDeleteFile(group.id, file.id);
}}
>
<FontAwesomeIcon icon={faTrash} />
</button>
</div>
</div> </div>
</div> ))}
))} </div>
</div> </div>
))} ))}
</div> </div>
<button className="toggle-button" onClick={onToggle}> <button className="toggle-button" onClick={onToggle}>
<FontAwesomeIcon icon={isOpen ? faChevronLeft : faChevronRight} /> <FontAwesomeIcon icon={isOpen ? faChevronLeft : faChevronRight} />
</button> </button>
<ConfirmDialog
isOpen={!!deleteConfirm}
title={`Delete ${deleteConfirm?.type === 'group' ? 'Group' : 'File'}`}
message={`Are you sure you want to delete ${deleteConfirm?.type} "${deleteConfirm?.name}"? This action cannot be undone.`}
onConfirm={confirmDelete}
onCancel={() => setDeleteConfirm(null)}
/>
</div> </div>
); );
}; };
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment