首次发布git

This commit is contained in:
2026-02-24 09:53:19 +08:00
commit dd97cf6a2c
71 changed files with 14875 additions and 0 deletions
+229
View File
@@ -0,0 +1,229 @@
"use client";
import { Page, useEditorStore } from "@/lib/store";
import { ChevronRight, FileText, Folder, FolderOpen, MoreHorizontal, File, Plus, Trash2, FilePlus, FolderPlus, Download } from "lucide-react";
import { cn } from "@/lib/utils";
import { useState } from "react";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
import { exportPageAsMarkdown, exportFolderAsZip } from "@/lib/export";
import { useRouter, usePathname } from "next/navigation";
import { useImport } from "@/components/import-context";
import { Upload } from "lucide-react";
import { useDroppable } from "@dnd-kit/core";
import { useSortable, SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
interface TreeViewProps {
pages: Page[];
parentId: string | null;
level?: number;
}
function TreeNode({ node, pages, level, expanded, toggleExpand }: {
node: Page,
pages: Page[],
level: number,
expanded: Record<string, boolean>,
toggleExpand: (id: string, e: React.MouseEvent) => void
}) {
const { activePageId, setActivePageId, addPage, deletePage } = useEditorStore();
const router = useRouter();
const pathname = usePathname();
const { triggerImport } = useImport();
const isFolder = node.type === 'folder';
const hasChildren = pages.some(p => p.parentId === node.id);
const isExpanded = expanded[node.id];
// DnD Hooks - Sorting
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({
id: node.id,
data: { type: node.type, title: node.title, parentId: node.parentId }
});
const style = {
paddingLeft: `${level * 12 + 8}px`,
transform: CSS.Translate.toString(transform),
transition,
opacity: isDragging ? 0.5 : 1,
};
return (
<div key={node.id} style={style} ref={setNodeRef} {...attributes} {...listeners}>
<div
className={cn(
"group flex items-center justify-between px-2 py-1 text-sm rounded-md transition-colors cursor-pointer select-none border border-transparent",
activePageId === node.id
? "bg-accent text-accent-foreground font-medium"
: "text-muted-foreground hover:bg-muted/50 hover:text-foreground",
// Use simple hover effect for "drop over" visual or rely on drag overlay
)}
onClick={() => {
setActivePageId(node.id);
if (pathname !== '/') router.push('/');
}}
>
<div className="flex items-center gap-1.5 flex-1 min-w-0">
<button
onPointerDown={(e) => e.stopPropagation()} // Prevent drag start on expand button
onClick={(e) => {
e.stopPropagation();
toggleExpand(node.id, e);
}}
className={cn(
"p-0.5 rounded-sm hover:bg-muted-foreground/20 transition-colors",
!hasChildren && !isFolder && "invisible"
)}
>
<ChevronRight
size={16}
className={cn("transition-transform shrink-0 text-muted-foreground/50", isExpanded && "rotate-90")}
/>
</button>
{node.icon ? (
<span className="text-[16px] leading-none shrink-0 w-4 h-4 flex items-center justify-center grayscale-[0.2]">{node.icon}</span>
) : isFolder ? (
isExpanded ? (
<FolderOpen size={16} className="text-indigo-500 fill-indigo-500/20 shrink-0" />
) : (
<Folder size={16} className="text-indigo-500 fill-indigo-500/20 shrink-0" />
)
) : (
<FileText size={16} className="text-muted-foreground/70 shrink-0" />
)}
<span className="truncate text-[14px] leading-none mb-0.5">{node.title}</span>
</div>
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild>
<button
onPointerDown={(e) => e.stopPropagation()} // Prevent drag
className="opacity-100 lg:opacity-0 lg:group-hover:opacity-100 p-1 hover:bg-muted-foreground/20 rounded text-muted-foreground transition-opacity"
>
<MoreHorizontal size={14} />
</button>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content className="min-w-[120px] bg-popover text-popover-foreground rounded-md border shadow-md p-1 z-50 animate-in fade-in zoom-in-95" align="start">
<DropdownMenu.Item
className="flex items-center gap-2 px-2 py-1.5 text-sm rounded hover:bg-accent cursor-pointer outline-none"
onClick={(e) => {
e.stopPropagation();
const targetParentId = node.type === 'folder' ? node.id : node.parentId;
// If creating sibling (node.type !== 'folder'), insert after current node
// Note: Order might be float/int. We just increment for now.
// Ideally we'd find the mid-point, but simpler logic: just +1 and let sort handle basic "after"
const order = node.type === 'folder' ? undefined : (node.order || 0) + 1;
addPage(targetParentId, 'file', undefined, order);
if (node.type === 'folder') toggleExpand(node.id, e);
}}
>
<FilePlus size={14} />
<span></span>
</DropdownMenu.Item>
<DropdownMenu.Item
className="flex items-center gap-2 px-2 py-1.5 text-sm rounded hover:bg-accent cursor-pointer outline-none"
onClick={(e) => {
e.stopPropagation();
const targetParentId = node.type === 'folder' ? node.id : node.parentId;
const order = node.type === 'folder' ? undefined : (node.order || 0) + 1;
addPage(targetParentId, 'folder', undefined, order);
if (node.type === 'folder') toggleExpand(node.id, e);
}}
>
<FolderPlus size={14} />
<span></span>
</DropdownMenu.Item>
<DropdownMenu.Item
className="flex items-center gap-2 px-2 py-1.5 text-sm rounded hover:bg-accent cursor-pointer outline-none"
onClick={(e) => {
e.stopPropagation();
const targetParentId = node.type === 'folder' ? node.id : node.parentId;
triggerImport(targetParentId);
if (node.type === 'folder') toggleExpand(node.id, e);
}}
>
<Upload size={14} />
<span>...</span>
</DropdownMenu.Item>
<DropdownMenu.Item
className="flex items-center gap-2 px-2 py-1.5 text-sm rounded hover:bg-accent cursor-pointer outline-none"
onClick={(e) => {
e.stopPropagation();
if (node.type === 'folder') {
exportFolderAsZip(node.id, pages, node.title);
} else {
exportPageAsMarkdown(node);
}
}}
>
<Download size={14} />
<span></span>
</DropdownMenu.Item>
<DropdownMenu.Separator className="h-px bg-muted my-1" />
<DropdownMenu.Item
className="flex items-center gap-2 px-2 py-1.5 text-sm rounded hover:bg-red-50 text-red-600 cursor-pointer outline-none"
onClick={(e) => {
e.stopPropagation();
if (confirm("确定要删除吗?此操作无法撤销。")) {
deletePage(node.id);
}
}}
>
<Trash2 size={14} />
<span></span>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
</div>
{(isExpanded || hasChildren) && isExpanded && (
<TreeView pages={pages} parentId={node.id} level={level + 1} />
)}
</div>
);
}
export function TreeView({ pages, parentId, level = 0 }: TreeViewProps) {
// Sort nodes by order field
const nodes = pages
.filter(p => p.parentId === parentId)
.sort((a, b) => (a.order || 0) - (b.order || 0));
// Simple state for expansion
const [expanded, setExpanded] = useState<Record<string, boolean>>({});
const toggleExpand = (id: string, e: React.MouseEvent) => {
setExpanded(prev => ({ ...prev, [id]: !prev[id] }));
};
if (nodes.length === 0) return null;
return (
<SortableContext
items={nodes.map(n => n.id)}
strategy={verticalListSortingStrategy}
>
<div className="flex flex-col gap-0.5">
{nodes.map(node => (
<TreeNode
key={node.id}
node={node}
pages={pages}
level={level}
expanded={expanded}
toggleExpand={toggleExpand}
/>
))}
</div>
</SortableContext>
);
}