对移动端的UI进行了重构
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
|
||||
import { useRouter, usePathname } from "next/navigation";
|
||||
import { useSortable, SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
||||
@@ -27,7 +26,43 @@ import { useConfirm } from "@/components/confirm-provider";
|
||||
interface TreeViewProps {
|
||||
pages: Page[];
|
||||
parentId: string | null;
|
||||
expanded: Record<string, boolean>;
|
||||
toggleExpand: (id: string) => void;
|
||||
level?: number;
|
||||
folderFileCount: Record<string, number>;
|
||||
}
|
||||
|
||||
function buildFolderFileCount(pages: Page[]): Record<string, number> {
|
||||
const byParent = new Map<string | null, Page[]>();
|
||||
for (const page of pages) {
|
||||
const siblings = byParent.get(page.parentId) || [];
|
||||
siblings.push(page);
|
||||
byParent.set(page.parentId, siblings);
|
||||
}
|
||||
|
||||
const counts: Record<string, number> = {};
|
||||
|
||||
const dfs = (folderId: string): number => {
|
||||
const children = byParent.get(folderId) || [];
|
||||
let total = 0;
|
||||
for (const child of children) {
|
||||
if (child.type === "file") {
|
||||
total += 1;
|
||||
} else {
|
||||
total += dfs(child.id);
|
||||
}
|
||||
}
|
||||
counts[folderId] = total;
|
||||
return total;
|
||||
};
|
||||
|
||||
for (const page of pages) {
|
||||
if (page.type === "folder") {
|
||||
dfs(page.id);
|
||||
}
|
||||
}
|
||||
|
||||
return counts;
|
||||
}
|
||||
|
||||
function TreeNode({
|
||||
@@ -36,12 +71,14 @@ function TreeNode({
|
||||
level,
|
||||
expanded,
|
||||
toggleExpand,
|
||||
folderFileCount,
|
||||
}: {
|
||||
node: Page;
|
||||
pages: Page[];
|
||||
level: number;
|
||||
expanded: Record<string, boolean>;
|
||||
toggleExpand: (id: string) => void;
|
||||
folderFileCount: Record<string, number>;
|
||||
}) {
|
||||
const { activePageId, setActivePageId, addPage, deletePage } = useEditorStore();
|
||||
const router = useRouter();
|
||||
@@ -52,6 +89,7 @@ function TreeNode({
|
||||
const isFolder = node.type === "folder";
|
||||
const hasChildren = pages.some((p) => p.parentId === node.id);
|
||||
const isExpanded = expanded[node.id];
|
||||
const isActive = activePageId === node.id;
|
||||
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||
id: node.id,
|
||||
@@ -59,7 +97,7 @@ function TreeNode({
|
||||
});
|
||||
|
||||
const style = {
|
||||
paddingLeft: `${level * 12 + 8}px`,
|
||||
paddingLeft: `${level * 12 + 6}px`,
|
||||
transform: CSS.Translate.toString(transform),
|
||||
transition,
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
@@ -69,17 +107,19 @@ function TreeNode({
|
||||
<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"
|
||||
"group relative flex h-10 items-center justify-between rounded-lg border border-transparent px-1.5 text-[13px] transition-colors cursor-pointer select-none md:h-8",
|
||||
isActive
|
||||
? "border-border/60 bg-accent/75 text-accent-foreground"
|
||||
: "text-muted-foreground hover:border-border/45 hover:bg-muted/55 hover:text-foreground"
|
||||
)}
|
||||
onClick={() => {
|
||||
setActivePageId(node.id);
|
||||
if (pathname !== "/") router.push("/");
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-1.5 flex-1 min-w-0">
|
||||
{isActive && <span className="absolute left-0 top-[8px] h-5 w-0.5 rounded-r bg-primary/90" />}
|
||||
|
||||
<div className="grid min-w-0 flex-1 grid-cols-[1.75rem_1.25rem_minmax(0,1fr)] items-center gap-1.5">
|
||||
<button
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
onClick={(e) => {
|
||||
@@ -87,36 +127,42 @@ function TreeNode({
|
||||
toggleExpand(node.id);
|
||||
}}
|
||||
className={cn(
|
||||
"p-0.5 rounded-sm hover:bg-muted-foreground/20 transition-colors",
|
||||
"flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground/70 transition-colors hover:bg-muted-foreground/15 hover:text-foreground md:h-6 md:w-6",
|
||||
!hasChildren && !isFolder && "invisible"
|
||||
)}
|
||||
>
|
||||
<ChevronRight
|
||||
size={16}
|
||||
className={cn("transition-transform shrink-0 text-muted-foreground/50", isExpanded && "rotate-90")}
|
||||
/>
|
||||
<ChevronRight size={14} className={cn("shrink-0 text-muted-foreground/60 transition-transform", 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" />
|
||||
<span className="flex h-5 w-5 items-center justify-center">
|
||||
{node.icon ? (
|
||||
<span className="flex h-5 w-5 items-center justify-center text-[16px] leading-none grayscale-[0.2]">{node.icon}</span>
|
||||
) : isFolder ? (
|
||||
isExpanded ? (
|
||||
<FolderOpen size={15} className="fill-indigo-500/20 text-indigo-500" />
|
||||
) : (
|
||||
<Folder size={15} className="fill-indigo-500/20 text-indigo-500" />
|
||||
)
|
||||
) : (
|
||||
<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>
|
||||
<FileText size={15} className="text-muted-foreground/70" />
|
||||
)}
|
||||
</span>
|
||||
<span className="flex min-w-0 items-center gap-1.5">
|
||||
<span className="truncate">{node.title || "无标题"}</span>
|
||||
{isFolder && (
|
||||
<span className="inline-flex min-w-[1.2rem] shrink-0 items-center justify-center rounded-full bg-muted px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground">
|
||||
{folderFileCount[node.id] ?? 0}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<button
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
className="opacity-100 lg:opacity-0 lg:group-hover:opacity-100 p-1 hover:bg-muted-foreground/20 rounded text-muted-foreground transition-opacity"
|
||||
className="flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground/75 transition-opacity hover:bg-muted-foreground/15 hover:text-foreground opacity-100 md:h-6 md:w-6 lg:opacity-0 lg:group-hover:opacity-100"
|
||||
>
|
||||
<MoreHorizontal size={14} />
|
||||
<MoreHorizontal size={13} />
|
||||
</button>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Portal>
|
||||
@@ -173,7 +219,7 @@ function TreeNode({
|
||||
<Download size={14} />
|
||||
<span>导出</span>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Separator className="h-px bg-muted my-1" />
|
||||
<DropdownMenu.Separator className="my-1 h-px bg-muted" />
|
||||
<DropdownMenu.Item
|
||||
className="flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-sm text-destructive outline-none transition-colors hover:bg-destructive/10"
|
||||
onClick={async (e) => {
|
||||
@@ -199,23 +245,25 @@ function TreeNode({
|
||||
</div>
|
||||
|
||||
{(isExpanded || hasChildren) && isExpanded && (
|
||||
<TreeView pages={pages} parentId={node.id} level={level + 1} />
|
||||
<TreeView
|
||||
pages={pages}
|
||||
parentId={node.id}
|
||||
level={level + 1}
|
||||
expanded={expanded}
|
||||
toggleExpand={toggleExpand}
|
||||
folderFileCount={folderFileCount}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function TreeView({ pages, parentId, level = 0 }: TreeViewProps) {
|
||||
export function TreeView({ pages, parentId, expanded, toggleExpand, level = 0, folderFileCount }: TreeViewProps) {
|
||||
const computedFolderFileCount = level === 0 ? buildFolderFileCount(pages) : folderFileCount;
|
||||
const nodes = pages
|
||||
.filter((p) => p.parentId === parentId)
|
||||
.sort((a, b) => (a.order || 0) - (b.order || 0));
|
||||
|
||||
const [expanded, setExpanded] = useState<Record<string, boolean>>({});
|
||||
|
||||
const toggleExpand = (id: string) => {
|
||||
setExpanded((prev) => ({ ...prev, [id]: !prev[id] }));
|
||||
};
|
||||
|
||||
if (nodes.length === 0) return null;
|
||||
|
||||
return (
|
||||
@@ -229,6 +277,7 @@ export function TreeView({ pages, parentId, level = 0 }: TreeViewProps) {
|
||||
level={level}
|
||||
expanded={expanded}
|
||||
toggleExpand={toggleExpand}
|
||||
folderFileCount={computedFolderFileCount}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user