对移动端的UI进行了重构

This commit is contained in:
2026-02-25 10:33:37 +08:00
parent ee3de1968c
commit 5709ef2966
16 changed files with 689 additions and 562 deletions
+83 -34
View File
@@ -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>