chore: save snapshot before tag 0.3
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
||||
import { Copy, Download, Hash, Link as LinkIcon, Move, PenLine, Trash2 } from "lucide-react";
|
||||
import type { MediaAsset } from "@/types/media";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface AssetContextMenuProps {
|
||||
asset: MediaAsset;
|
||||
position: { x: number; y: number };
|
||||
onClose: () => void;
|
||||
onOpen: (asset: MediaAsset) => void;
|
||||
onCopyLink: (asset: MediaAsset) => void;
|
||||
onCopyPath: (asset: MediaAsset) => void;
|
||||
onRename: (asset: MediaAsset) => void;
|
||||
onMove: (asset: MediaAsset) => void;
|
||||
onDelete: (assetIds: string[]) => void;
|
||||
onDownload: (asset: MediaAsset) => void;
|
||||
}
|
||||
|
||||
export function AssetContextMenu({
|
||||
asset,
|
||||
position,
|
||||
onClose,
|
||||
onOpen,
|
||||
onCopyLink,
|
||||
onCopyPath,
|
||||
onRename,
|
||||
onMove,
|
||||
onDelete,
|
||||
onDownload,
|
||||
}: AssetContextMenuProps) {
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
const [pos, setPos] = useState(position);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const clampPosition = () => {
|
||||
const element = menuRef.current;
|
||||
if (!element) {
|
||||
setPos(position);
|
||||
return;
|
||||
}
|
||||
const rect = element.getBoundingClientRect();
|
||||
const padding = 12;
|
||||
const maxLeft = Math.max(padding, window.innerWidth - rect.width - padding);
|
||||
const maxTop = Math.max(padding, window.innerHeight - rect.height - padding);
|
||||
const left = Math.min(Math.max(padding, position.x), maxLeft);
|
||||
const top = Math.min(Math.max(padding, position.y), maxTop);
|
||||
setPos({ x: left, y: top });
|
||||
};
|
||||
clampPosition();
|
||||
window.addEventListener("resize", clampPosition);
|
||||
return () => window.removeEventListener("resize", clampPosition);
|
||||
}, [position]);
|
||||
|
||||
useEffect(() => {
|
||||
const close = () => onClose();
|
||||
window.addEventListener("click", close);
|
||||
return () => window.removeEventListener("click", close);
|
||||
}, [onClose]);
|
||||
|
||||
const buttonClass =
|
||||
"flex w-full items-center gap-2 rounded-sm px-3 py-2 text-left text-sm text-gray-700 hover:bg-[#f5f7fb]";
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={menuRef}
|
||||
className="fixed z-50 rounded-xl border border-[#ececec] bg-white p-1 text-sm text-gray-700 shadow-2xl"
|
||||
style={{ top: pos.y, left: pos.x, minWidth: 200 }}
|
||||
>
|
||||
<button type="button" className={buttonClass} onClick={() => onOpen(asset)}>
|
||||
<LinkIcon className="h-4 w-4 text-[#2563eb]" />
|
||||
<span>在新标签打开</span>
|
||||
</button>
|
||||
<button type="button" className={buttonClass} onClick={() => onDownload(asset)}>
|
||||
<Download className="h-4 w-4 text-gray-500" />
|
||||
<span>下载</span>
|
||||
</button>
|
||||
<button type="button" className={buttonClass} onClick={() => onCopyLink(asset)}>
|
||||
<Copy className="h-4 w-4 text-gray-500" />
|
||||
<span>复制链接</span>
|
||||
</button>
|
||||
<button type="button" className={buttonClass} onClick={() => onCopyPath(asset)}>
|
||||
<Hash className="h-4 w-4 text-gray-500" />
|
||||
<span>复制存储路径</span>
|
||||
</button>
|
||||
<div className="my-1 border-t border-[#f2f2f2]" />
|
||||
<button type="button" className={buttonClass} onClick={() => onRename(asset)}>
|
||||
<PenLine className="h-4 w-4 text-gray-500" />
|
||||
<span>重命名</span>
|
||||
</button>
|
||||
<button type="button" className={buttonClass} onClick={() => onMove(asset)}>
|
||||
<Move className="h-4 w-4 text-gray-500" />
|
||||
<span>移动到...</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="mt-1 flex w-full items-center gap-2 rounded-sm px-3 py-2 text-left text-sm text-red-500 hover:bg-red-50"
|
||||
onClick={() => onDelete([asset.id])}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
<span>删除</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
"use client";
|
||||
|
||||
import { ChevronRight, FileText, Folder, Paperclip, Plus } from "lucide-react";
|
||||
import type { DocumentNode } from "@/lib/documents";
|
||||
import type { MediaAsset } from "@/types/media";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useMemo } from "react";
|
||||
|
||||
interface FileTreeProps {
|
||||
nodes: DocumentNode[];
|
||||
assetsByDoc: Record<string, MediaAsset[]>;
|
||||
expanded: Set<string>;
|
||||
activeId: string;
|
||||
onToggleExpand: (id: string) => void;
|
||||
onOpenDocument: (id: string) => void;
|
||||
onOpenAsset: (asset: MediaAsset) => void;
|
||||
onCreateChild: (parentId: string | null) => void;
|
||||
onContextMenu: (event: React.MouseEvent, node: DocumentNode) => void;
|
||||
selectedAssetIds?: Set<string>;
|
||||
onToggleAssetSelect?: (assetId: string) => void;
|
||||
onSelectOnlyAsset?: (assetId: string) => void;
|
||||
disableSelection?: boolean;
|
||||
onDropFiles?: (docId: string, files: FileList) => void;
|
||||
onAssetContextMenu?: (event: React.MouseEvent, asset: MediaAsset) => void;
|
||||
}
|
||||
|
||||
const INDENT = 16;
|
||||
|
||||
export function FileTree({
|
||||
nodes,
|
||||
assetsByDoc,
|
||||
expanded,
|
||||
activeId,
|
||||
onToggleExpand,
|
||||
onOpenDocument,
|
||||
onOpenAsset,
|
||||
onCreateChild,
|
||||
onContextMenu,
|
||||
selectedAssetIds = new Set<string>(),
|
||||
onToggleAssetSelect,
|
||||
onSelectOnlyAsset,
|
||||
disableSelection = false,
|
||||
onDropFiles,
|
||||
onAssetContextMenu,
|
||||
}: FileTreeProps) {
|
||||
if (nodes.length === 0) {
|
||||
return <div className="px-4 py-6 text-sm text-gray-400">暂无页面,点击下方按钮创建</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="divide-y divide-[#f4f4f5]"
|
||||
onDragOver={(e) => {
|
||||
if (onDropFiles) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
onDrop={(e) => {
|
||||
if (onDropFiles && e.dataTransfer.files?.length) {
|
||||
e.preventDefault();
|
||||
const files = e.dataTransfer.files;
|
||||
const targetDoc = nodes[0]?.id ?? "";
|
||||
onDropFiles(targetDoc, files);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{nodes.map((node) => (
|
||||
<FileTreeNode
|
||||
key={node.id}
|
||||
node={node}
|
||||
depth={0}
|
||||
assetsByDoc={assetsByDoc}
|
||||
expanded={expanded}
|
||||
activeId={activeId}
|
||||
onToggleExpand={onToggleExpand}
|
||||
onOpenDocument={onOpenDocument}
|
||||
onOpenAsset={onOpenAsset}
|
||||
onCreateChild={onCreateChild}
|
||||
onContextMenu={onContextMenu}
|
||||
selectedAssetIds={selectedAssetIds}
|
||||
onToggleAssetSelect={onToggleAssetSelect}
|
||||
onSelectOnlyAsset={onSelectOnlyAsset}
|
||||
disableSelection={disableSelection}
|
||||
onDropFiles={onDropFiles}
|
||||
onAssetContextMenu={onAssetContextMenu}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface FileTreeNodeProps {
|
||||
node: DocumentNode;
|
||||
depth: number;
|
||||
assetsByDoc: Record<string, MediaAsset[]>;
|
||||
expanded: Set<string>;
|
||||
activeId: string;
|
||||
onToggleExpand: (id: string) => void;
|
||||
onOpenDocument: (id: string) => void;
|
||||
onOpenAsset: (asset: MediaAsset) => void;
|
||||
onCreateChild: (parentId: string | null) => void;
|
||||
onContextMenu: (event: React.MouseEvent, node: DocumentNode) => void;
|
||||
selectedAssetIds: Set<string>;
|
||||
onToggleAssetSelect?: (assetId: string) => void;
|
||||
onSelectOnlyAsset?: (assetId: string) => void;
|
||||
disableSelection: boolean;
|
||||
onDropFiles?: (docId: string, files: FileList) => void;
|
||||
onAssetContextMenu?: (event: React.MouseEvent, asset: MediaAsset) => void;
|
||||
}
|
||||
|
||||
function FileTreeNode({
|
||||
node,
|
||||
depth,
|
||||
assetsByDoc,
|
||||
expanded,
|
||||
activeId,
|
||||
onToggleExpand,
|
||||
onOpenDocument,
|
||||
onOpenAsset,
|
||||
onCreateChild,
|
||||
onContextMenu,
|
||||
selectedAssetIds,
|
||||
onToggleAssetSelect,
|
||||
onSelectOnlyAsset,
|
||||
disableSelection,
|
||||
onDropFiles,
|
||||
onAssetContextMenu,
|
||||
}: FileTreeNodeProps) {
|
||||
const isExpanded = expanded.has(node.id);
|
||||
const assets = useMemo(() => assetsByDoc[node.id] ?? [], [assetsByDoc, node.id]);
|
||||
const hasChildren = node.children.length > 0 || assets.length > 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="py-0.5"
|
||||
onDragOver={(e) => {
|
||||
if (onDropFiles) e.preventDefault();
|
||||
}}
|
||||
onDrop={(e) => {
|
||||
if (onDropFiles && e.dataTransfer.files?.length) {
|
||||
e.preventDefault();
|
||||
onDropFiles(node.id, e.dataTransfer.files);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-2 rounded-md px-2 py-1.5 text-sm text-gray-700 hover:bg-[#f5f7fb]",
|
||||
activeId === node.id && "bg-[#e8f2ff] text-[#2563eb]",
|
||||
)}
|
||||
style={{ paddingLeft: depth * INDENT + 8 }}
|
||||
onContextMenu={(event) => onContextMenu(event, node)}
|
||||
>
|
||||
{hasChildren ? (
|
||||
<button
|
||||
type="button"
|
||||
aria-label="展开或折叠"
|
||||
onClick={() => onToggleExpand(node.id)}
|
||||
className="flex h-5 w-5 items-center justify-center rounded-md hover:bg-gray-100"
|
||||
>
|
||||
<ChevronRight className={cn("h-3.5 w-3.5 transition-transform", isExpanded && "rotate-90")} />
|
||||
</button>
|
||||
) : (
|
||||
<span className="h-5 w-5" />
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="flex flex-1 items-center gap-2 text-left"
|
||||
onClick={() => onOpenDocument(node.id)}
|
||||
>
|
||||
<Folder className="h-4 w-4 text-[#2563eb]" />
|
||||
<span className="truncate">{node.title || "无标题"}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="新建子页面"
|
||||
onClick={() => onCreateChild(node.id)}
|
||||
className="rounded-md p-1 text-gray-400 hover:bg-gray-100 hover:text-gray-700"
|
||||
>
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isExpanded && (
|
||||
<div className="space-y-0.5">
|
||||
<FileLeafRow
|
||||
depth={depth + 1}
|
||||
label="index.md"
|
||||
icon={<FileText className="h-4 w-4 text-gray-500" />}
|
||||
onClick={() => onOpenDocument(node.id)}
|
||||
stopBubble
|
||||
/>
|
||||
{assets.map((asset) => (
|
||||
<FileLeafRow
|
||||
key={asset.id}
|
||||
depth={depth + 1}
|
||||
label={asset.file_name || "附件"}
|
||||
icon={<Paperclip className="h-4 w-4 text-gray-500" />}
|
||||
selected={selectedAssetIds.has(asset.id)}
|
||||
selectable={!disableSelection}
|
||||
onSelectToggle={
|
||||
onToggleAssetSelect ? () => onToggleAssetSelect(asset.id) : undefined
|
||||
}
|
||||
onSelectOnly={onSelectOnlyAsset ? () => onSelectOnlyAsset(asset.id) : undefined}
|
||||
onClick={() => onOpenAsset(asset)}
|
||||
stopBubble
|
||||
onContextMenu={
|
||||
onAssetContextMenu
|
||||
? (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onAssetContextMenu(e, asset);
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
))}
|
||||
{node.children.map((child) => (
|
||||
<FileTreeNode
|
||||
key={child.id}
|
||||
node={child}
|
||||
depth={depth + 1}
|
||||
assetsByDoc={assetsByDoc}
|
||||
expanded={expanded}
|
||||
activeId={activeId}
|
||||
onToggleExpand={onToggleExpand}
|
||||
onOpenDocument={onOpenDocument}
|
||||
onOpenAsset={onOpenAsset}
|
||||
onCreateChild={onCreateChild}
|
||||
onContextMenu={onContextMenu}
|
||||
selectedAssetIds={selectedAssetIds}
|
||||
onToggleAssetSelect={onToggleAssetSelect}
|
||||
onSelectOnlyAsset={onSelectOnlyAsset}
|
||||
disableSelection={disableSelection}
|
||||
onDropFiles={onDropFiles}
|
||||
onAssetContextMenu={onAssetContextMenu}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FileLeafRow({
|
||||
depth,
|
||||
label,
|
||||
icon,
|
||||
onClick,
|
||||
selected = false,
|
||||
selectable = false,
|
||||
onSelectToggle,
|
||||
onSelectOnly,
|
||||
stopBubble = false,
|
||||
onContextMenu,
|
||||
}: {
|
||||
depth: number;
|
||||
label: string;
|
||||
icon: React.ReactNode;
|
||||
onClick: () => void;
|
||||
selected?: boolean;
|
||||
selectable?: boolean;
|
||||
onSelectToggle?: () => void;
|
||||
onSelectOnly?: () => void;
|
||||
stopBubble?: boolean;
|
||||
onContextMenu?: (event: React.MouseEvent) => void;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className="flex items-center gap-2 rounded-md px-2 py-1 text-sm text-gray-600 hover:bg-[#f8fafc]"
|
||||
style={{ paddingLeft: depth * INDENT + 32 }}
|
||||
onClick={(e) => {
|
||||
if (stopBubble) e.stopPropagation();
|
||||
// 附件行默认只负责选中,不直接打开,避免误触下载
|
||||
if (selectable) return;
|
||||
onClick();
|
||||
}}
|
||||
onMouseDown={(e) => {
|
||||
if (stopBubble) e.stopPropagation();
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onContextMenu={(e) => {
|
||||
if (stopBubble) e.stopPropagation();
|
||||
if (onContextMenu) onContextMenu(e);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter" || event.key === " ") {
|
||||
event.preventDefault();
|
||||
onClick();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{selectable ? (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selected}
|
||||
onChange={(e) => {
|
||||
e.stopPropagation();
|
||||
if (onSelectToggle) onSelectToggle();
|
||||
}}
|
||||
className="h-4 w-4 rounded border-gray-300 text-[#2563eb]"
|
||||
/>
|
||||
) : (
|
||||
<span className="h-4 w-4" />
|
||||
)}
|
||||
{icon}
|
||||
<button
|
||||
type="button"
|
||||
className="flex-1 truncate text-left"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onClick();
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -42,6 +42,8 @@ import { PrivateTree } from "@/components/sidebar/private-tree";
|
||||
import { buildSidebarSectionsFromTree, flattenDocumentTree } from "@/lib/sidebar-tree";
|
||||
import { supabaseBrowser } from "@/lib/supabase/client";
|
||||
import { useSearchPaletteStore } from "@/store/search-palette";
|
||||
import { FileTree } from "@/components/sidebar/file-tree";
|
||||
import type { MediaAsset } from "@/types/media";
|
||||
|
||||
const TOP_BUTTONS = [
|
||||
{ id: "search", icon: SearchIcon, label: "搜索" },
|
||||
@@ -74,7 +76,9 @@ interface ContextMenuState {
|
||||
export function Sidebar({ initialData }: SidebarProps) {
|
||||
const { open, setOpen, width, setWidth, collapsedSections, toggleSection, trashConfirm, setTrashConfirm } =
|
||||
useSidebarStore();
|
||||
const openSearchPalette = useSearchPaletteStore((state) => state.openSearch);
|
||||
const viewMode = useSidebarStore((state) => state.viewMode);
|
||||
const setViewMode = useSidebarStore((state) => state.setViewMode);
|
||||
const openSearchPalette = useSearchPaletteStore((state) => state.openSearch);
|
||||
const sidebarQuery = useSidebarData(initialData);
|
||||
const sidebarData = sidebarQuery.data ?? initialData;
|
||||
const segments = useSelectedLayoutSegments();
|
||||
@@ -148,6 +152,18 @@ export function Sidebar({ initialData }: SidebarProps) {
|
||||
);
|
||||
}, [sidebarData.trashedDocuments, trashSearch]);
|
||||
|
||||
const assetsByDoc = useMemo(() => {
|
||||
const map: Record<string, MediaAsset[]> = {};
|
||||
const assets = sidebarData.mediaAssets ?? [];
|
||||
assets.forEach((asset) => {
|
||||
if (!map[asset.document_id]) {
|
||||
map[asset.document_id] = [];
|
||||
}
|
||||
map[asset.document_id].push(asset);
|
||||
});
|
||||
return map;
|
||||
}, [sidebarData.mediaAssets]);
|
||||
|
||||
const activeWorkspace =
|
||||
sidebarData.workspaces.find((workspace) => workspace.id === sidebarData.activeWorkspaceId) ??
|
||||
sidebarData.workspaces[0];
|
||||
@@ -231,6 +247,17 @@ export function Sidebar({ initialData }: SidebarProps) {
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleOpenAsset = useCallback((asset: MediaAsset) => {
|
||||
const url = asset.signed_url ?? asset.file_url;
|
||||
if (!url) {
|
||||
window.alert("暂无可用的文件链接");
|
||||
return;
|
||||
}
|
||||
if (typeof window !== "undefined") {
|
||||
window.open(url, "_blank", "noopener,noreferrer");
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleResizeStart = useCallback(
|
||||
(event: React.MouseEvent) => {
|
||||
event.preventDefault();
|
||||
@@ -577,74 +604,120 @@ export function Sidebar({ initialData }: SidebarProps) {
|
||||
<Library className="h-4 w-4" />
|
||||
页面树
|
||||
</div>
|
||||
<Input
|
||||
className="mt-2 h-8 rounded-md border-[#eeeeee]"
|
||||
placeholder="搜索页面"
|
||||
value={filter}
|
||||
onChange={(event) => setFilter(event.target.value)}
|
||||
/>
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<Input
|
||||
className="h-8 flex-1 rounded-md border-[#eeeeee]"
|
||||
placeholder="搜索页面"
|
||||
value={filter}
|
||||
onChange={(event) => setFilter(event.target.value)}
|
||||
/>
|
||||
<div className="flex rounded-md border border-[#e5e7eb] bg-white">
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"px-3 py-1 text-xs",
|
||||
viewMode === "section" ? "bg-[#2563eb] text-white" : "text-gray-600",
|
||||
)}
|
||||
onClick={() => setViewMode("section")}
|
||||
>
|
||||
分组
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"px-3 py-1 text-xs",
|
||||
viewMode === "filesystem" ? "bg-[#2563eb] text-white" : "text-gray-600",
|
||||
)}
|
||||
onClick={() => setViewMode("filesystem")}
|
||||
>
|
||||
文件
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SectionList
|
||||
id="starred"
|
||||
label="星标置顶"
|
||||
icon={SECTION_ICONS.starred}
|
||||
nodes={starredNodes}
|
||||
collapsed={collapsedSections.starred}
|
||||
onToggle={() => toggleSection("starred")}
|
||||
/>
|
||||
<SectionList
|
||||
id="public"
|
||||
label="公共页面"
|
||||
icon={SECTION_ICONS.public}
|
||||
nodes={publicNodes}
|
||||
collapsed={collapsedSections.public}
|
||||
onToggle={() => toggleSection("public")}
|
||||
/>
|
||||
<SectionList
|
||||
id="shared"
|
||||
label="共享页面"
|
||||
icon={SECTION_ICONS.shared}
|
||||
nodes={sharedNodes}
|
||||
collapsed={collapsedSections.shared}
|
||||
onToggle={() => toggleSection("shared")}
|
||||
/>
|
||||
<SectionList
|
||||
id="templates"
|
||||
label="模板中心"
|
||||
icon={SECTION_ICONS.templates}
|
||||
nodes={templateNodes}
|
||||
collapsed={collapsedSections.templates}
|
||||
onToggle={() => toggleSection("templates")}
|
||||
/>
|
||||
{viewMode === "section" ? (
|
||||
<>
|
||||
<SectionList
|
||||
id="starred"
|
||||
label="星标置顶"
|
||||
icon={SECTION_ICONS.starred}
|
||||
nodes={starredNodes}
|
||||
collapsed={collapsedSections.starred}
|
||||
onToggle={() => toggleSection("starred")}
|
||||
/>
|
||||
<SectionList
|
||||
id="public"
|
||||
label="公共页面"
|
||||
icon={SECTION_ICONS.public}
|
||||
nodes={publicNodes}
|
||||
collapsed={collapsedSections.public}
|
||||
onToggle={() => toggleSection("public")}
|
||||
/>
|
||||
<SectionList
|
||||
id="shared"
|
||||
label="共享页面"
|
||||
icon={SECTION_ICONS.shared}
|
||||
nodes={sharedNodes}
|
||||
collapsed={collapsedSections.shared}
|
||||
onToggle={() => toggleSection("shared")}
|
||||
/>
|
||||
<SectionList
|
||||
id="templates"
|
||||
label="模板中心"
|
||||
icon={SECTION_ICONS.templates}
|
||||
nodes={templateNodes}
|
||||
collapsed={collapsedSections.templates}
|
||||
onToggle={() => toggleSection("templates")}
|
||||
/>
|
||||
|
||||
<div className="flex flex-1 flex-col border-t border-[#f1f1f1]">
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center justify-between px-4 py-3 text-sm font-medium text-gray-600"
|
||||
onClick={() => toggleSection("private")}
|
||||
>
|
||||
<span>私有 / 我的页面</span>
|
||||
<MoreHorizontal className="h-4 w-4 text-gray-400" />
|
||||
</button>
|
||||
{!collapsedSections.private ? (
|
||||
<div className="flex flex-1 flex-col border-t border-[#f1f1f1]">
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center justify-between px-4 py-3 text-sm font-medium text-gray-600"
|
||||
onClick={() => toggleSection("private")}
|
||||
>
|
||||
<span>私有 / 我的页面</span>
|
||||
<MoreHorizontal className="h-4 w-4 text-gray-400" />
|
||||
</button>
|
||||
{!collapsedSections.private ? (
|
||||
<div className="flex-1 px-1 pb-2">
|
||||
<div className="h-full rounded-md border border-[#eff2f6] bg-white">
|
||||
<PrivateTree
|
||||
nodes={filteredPrivateTree}
|
||||
expanded={expanded}
|
||||
activeId={activeId}
|
||||
onToggleExpand={toggleExpand}
|
||||
onMove={handleMove}
|
||||
onCreateChild={handleCreate}
|
||||
onContextMenu={openContextMenu}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="px-4 pb-2 text-xs text-gray-400">已折叠</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex flex-1 flex-col border-t border-[#f1f1f1]">
|
||||
<div className="flex-1 px-1 pb-2">
|
||||
<div className="h-full rounded-md border border-[#eff2f6] bg-white">
|
||||
<PrivateTree
|
||||
<FileTree
|
||||
nodes={filteredPrivateTree}
|
||||
assetsByDoc={assetsByDoc}
|
||||
expanded={expanded}
|
||||
activeId={activeId}
|
||||
onToggleExpand={toggleExpand}
|
||||
onMove={handleMove}
|
||||
onOpenDocument={(id) => handleOpenDocument(id, "main")}
|
||||
onOpenAsset={handleOpenAsset}
|
||||
onCreateChild={handleCreate}
|
||||
onContextMenu={openContextMenu}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="px-4 pb-2 text-xs text-gray-400">已折叠</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="border-t border-[#f1f1f1] p-3">
|
||||
<Button className="w-full justify-center gap-2" variant="outline" onClick={() => handleCreate(null)}>
|
||||
|
||||
Reference in New Issue
Block a user