Commit 304fdc2c by wanjia

迭代一版

parent b5b01eb1
......@@ -3,13 +3,13 @@ const { contextBridge, ipcRenderer } = require('electron');
// 暴露安全的 API 到渲染进程
contextBridge.exposeInMainWorld('electronAPI', {
// 文件系统操作
getGroups: () => ipcRenderer.invoke('get-groups'),
createGroup: (groupName) => ipcRenderer.invoke('create-group', groupName),
deleteGroup: (groupId) => ipcRenderer.invoke('delete-group', groupId),
createFile: (groupId, fileName) => ipcRenderer.invoke('create-file', groupId, fileName),
deleteFile: (groupId, fileId) => ipcRenderer.invoke('delete-file', groupId, fileId),
saveFile: (fileId, content) => ipcRenderer.invoke('save-file', fileId, content),
loadFile: (fileId) => ipcRenderer.invoke('load-file', fileId),
getGroups: () => ipcRenderer.invoke('get-groups'),
loadFile: (fileId) => ipcRenderer.invoke('load-file-content', fileId),
saveFile: (fileId, content) => ipcRenderer.invoke('save-file-content', fileId, content),
// 系统信息
platform: process.platform
......
......@@ -27,11 +27,19 @@ import './components/Sidebar.css';
type ViewMode = 'split' | 'edit' | 'preview';
interface Document {
id: string;
groupId: string;
name: string;
content: string;
}
const App: React.FC = () => {
const editorRef = useRef<any>(null);
const [markdown, setMarkdown] = useState<string>('');
const [viewMode, setViewMode] = useState<ViewMode>('split');
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
const [currentDocument, setCurrentDocument] = useState<Document | null>(null);
const handleEditorDidMount = (editor: any) => {
editorRef.current = editor;
......@@ -39,6 +47,9 @@ const App: React.FC = () => {
const handleEditorChange = (value: string | undefined) => {
setMarkdown(value || '');
if (currentDocument) {
window.electronAPI.saveFile(currentDocument.id, value || '');
}
};
const insertAtCursor = (before: string, after: string = '') => {
......@@ -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 = () => {
setIsSidebarOpen(!isSidebarOpen);
};
return (
<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="toolbar">
<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 @@
}
.group {
margin-bottom: 10px;
margin: 8px 0;
}
.group-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 8px;
background-color: #e8e8e8;
padding: 8px 12px;
cursor: pointer;
border-radius: 4px;
margin-bottom: 4px;
transition: background-color 0.2s;
}
.group-header:hover {
background-color: rgba(255, 255, 255, 0.1);
}
.group-name {
font-weight: 500;
color: #333;
display: flex;
align-items: center;
gap: 6px;
gap: 8px;
font-weight: 500;
}
.group-actions {
display: flex;
gap: 4px;
.group-toggle-icon {
width: 12px;
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 {
display: flex;
align-items: center;
justify-content: space-between;
padding: 4px 8px;
margin-left: 8px;
border-radius: 4px;
padding: 8px 12px;
margin: 2px 0;
cursor: pointer;
border-radius: 4px;
transition: background-color 0.2s;
}
.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 {
......
import React, { useState, useEffect } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import {
faChevronLeft,
faChevronRight,
faChevronDown,
faFolder,
faFolderPlus,
faFile,
faPlus,
faTrash
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import './Sidebar.css';
import ConfirmDialog from './ConfirmDialog';
interface SidebarProps {
isOpen: boolean;
onToggle: () => void;
onDocumentSelect: (groupId: string, fileId: string) => void;
currentDocumentId: string | undefined;
}
interface Group {
......@@ -28,12 +32,25 @@ interface File {
content: string;
}
const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle }) => {
const Sidebar: React.FC<SidebarProps> = ({
isOpen,
onToggle,
onDocumentSelect,
currentDocumentId
}) => {
const [groups, setGroups] = useState<Group[]>([]);
const [showNewGroupForm, setShowNewGroupForm] = useState(false);
const [newGroupName, setNewGroupName] = useState('');
const [showNewFileForm, setShowNewFileForm] = useState<string | null>(null);
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(() => {
// 从后端加载组和文件
......@@ -55,12 +72,15 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle }) => {
};
const handleDeleteGroup = async (groupId: string) => {
try {
await window.electronAPI.deleteGroup(groupId);
setGroups(groups.filter(group => group.id !== groupId));
} catch (error) {
console.error('Failed to delete group:', error);
}
const group = groups.find(g => g.id === groupId);
if (!group) return;
setDeleteConfirm({
type: 'group',
groupId,
name: group.name,
isOpen: true
});
};
const handleCreateFile = async (groupId: string) => {
......@@ -78,26 +98,64 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle }) => {
}));
setNewFileName('');
setShowNewFileForm(null);
// 创建后自动选择新文件
onDocumentSelect(groupId, newFile.id);
} catch (error) {
console.error('Failed to create file:', error);
}
};
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 {
await window.electronAPI.deleteFile(groupId, fileId);
if (deleteConfirm.type === 'group') {
await window.electronAPI.deleteGroup(deleteConfirm.groupId);
setGroups(groups.filter(g => g.id !== deleteConfirm.groupId));
} else {
if (!deleteConfirm.fileId) return;
await window.electronAPI.deleteFile(deleteConfirm.groupId, deleteConfirm.fileId);
setGroups(groups.map(group => {
if (group.id === groupId) {
if (group.id === deleteConfirm.groupId) {
return {
...group,
files: group.files.filter(file => file.id !== fileId)
files: group.files.filter(f => f.id !== deleteConfirm.fileId)
};
}
return group;
}));
} catch (error) {
console.error('Failed to delete file:', error);
}
} 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 (
......@@ -122,15 +180,31 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle }) => {
{groups.map(group => (
<div key={group.id} className="group">
<div className="group-header">
<div className="group-header" onClick={() => toggleGroup(group.id)}>
<span className="group-name">
<FontAwesomeIcon
icon={collapsedGroups.has(group.id) ? faChevronRight : faChevronDown}
className="group-toggle-icon"
/>
<FontAwesomeIcon icon={faFolder} /> {group.name}
</span>
<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} />
</button>
<button className="action-button" onClick={() => handleDeleteGroup(group.id)}>
<button
className="action-button"
onClick={(e) => {
e.stopPropagation();
handleDeleteGroup(group.id);
}}
>
<FontAwesomeIcon icon={faTrash} />
</button>
</div>
......@@ -149,15 +223,23 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle }) => {
</div>
)}
<div className={`group-files ${collapsedGroups.has(group.id) ? 'collapsed' : ''}`}>
{group.files.map(file => (
<div key={file.id} className="file-item">
<div
key={file.id}
className={`file-item ${currentDocumentId === file.id ? 'active' : ''}`}
onClick={() => onDocumentSelect(group.id, file.id)}
>
<span className="file-name">
<FontAwesomeIcon icon={faFile} /> {file.name}
</span>
<div className="file-actions">
<button
className="action-button"
onClick={() => handleDeleteFile(group.id, file.id)}
onClick={(e) => {
e.stopPropagation();
handleDeleteFile(group.id, file.id);
}}
>
<FontAwesomeIcon icon={faTrash} />
</button>
......@@ -165,11 +247,20 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle }) => {
</div>
))}
</div>
</div>
))}
</div>
<button className="toggle-button" onClick={onToggle}>
<FontAwesomeIcon icon={isOpen ? faChevronLeft : faChevronRight} />
</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>
);
};
......
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