0.1.14 上线前更改

This commit is contained in:
liaibo
2026-01-11 12:35:53 +08:00
parent be71849aa5
commit 725a60d3aa
44 changed files with 3427 additions and 310 deletions
+222 -45
View File
@@ -6,10 +6,8 @@ import { useRouter, useSelectedLayoutSegments } from "next/navigation";
import {
ArrowRightLeft,
ArrowUpRight,
Bell,
ChevronRight,
Copy,
Dice5,
Edit3,
GitMerge,
Globe,
@@ -19,7 +17,6 @@ import {
Link as LinkIcon,
MoreHorizontal,
PanelRightOpen,
PenSquare,
Plus,
Search as SearchIcon,
Share2,
@@ -66,10 +63,10 @@ const TOP_BUTTONS = [
{ id: "graph", icon: Share2, label: "关系图" },
{ id: "import", icon: Upload, label: "导入" },
{ id: "members", icon: Users, label: "成员" },
{ id: "inbox", icon: Bell, label: "消息箱" },
{ id: "quick-note", icon: PenSquare, label: "今日速记" },
{ id: "lucky", icon: Dice5, label: "手气不错" },
{ id: "more", icon: MoreHorizontal, label: "更多" },
{ id: "starred", icon: Star, label: "星标置顶" },
{ id: "public", icon: Globe, label: "公共页面" },
{ id: "shared", icon: Shield, label: "共享页面" },
{ id: "templates", icon: LayoutGrid, label: "模板中心" },
] as const;
const SECTION_ICONS: Record<Exclude<SidebarSectionId, "private">, React.ReactNode> = {
@@ -79,6 +76,29 @@ const SECTION_ICONS: Record<Exclude<SidebarSectionId, "private">, React.ReactNod
templates: <LayoutGrid className="h-4 w-4 text-[#34d399]" />,
};
const normalizeStoragePath = (storagePath: string): string => storagePath.replaceAll("\\", "/");
const extractMindmapIdFromStoragePath = (
storagePath: string | null | undefined,
): string | null => {
if (!storagePath) return null;
const normalized = normalizeStoragePath(storagePath);
const prefix = "mindmaps/";
if (normalized.startsWith(prefix)) {
const rest = normalized.slice(prefix.length);
const id = rest.split("/")[0];
return id ? id : null;
}
const marker = "/mindmaps/";
const idx = normalized.indexOf(marker);
if (idx === -1) return null;
const rest = normalized.slice(idx + marker.length);
const id = rest.split("/")[0];
return id ? id : null;
};
interface SidebarProps {
initialData: SidebarInitialData;
}
@@ -90,8 +110,11 @@ interface ContextMenuState {
}
export function Sidebar({ initialData }: SidebarProps) {
const { open, setOpen, width, setWidth, collapsedSections, toggleSection, trashConfirm, setTrashConfirm } =
const { open, setOpen, width, setWidth, collapsedSections, toggleSection, setSectionCollapsed, trashConfirm, setTrashConfirm } =
useSidebarStore();
const sectionsTrayOpen = useSidebarStore((state) => state.sectionsTrayOpen);
const toggleSectionsTray = useSidebarStore((state) => state.toggleSectionsTray);
const setSectionsTrayOpen = useSidebarStore((state) => state.setSectionsTrayOpen);
const viewMode = useSidebarStore((state) => state.viewMode);
const setViewMode = useSidebarStore((state) => state.setViewMode);
const openSearchPalette = useSearchPaletteStore((state) => state.openSearch);
@@ -274,9 +297,67 @@ export function Sidebar({ initialData }: SidebarProps) {
return assets.filter((item) => (item.file_name ?? "未命名附件").toLowerCase().includes(keyword));
}, [sidebarData.trashedMediaAssets, sidebarData.trashedMindmapAssets, trashSearch]);
const mindmapChildrenSnapshot = useMemo(() => {
const mapping = sidebarData.mindmapAssetChildren ?? {};
const mindmapDocById = new Map<string, string>(
(mindmapAssets ?? [])
.filter((asset) => asset.asset_type === "mindmap")
.map((asset) => [asset.id, asset.document_id]),
);
const mindmapIds = new Set(mindmapDocById.keys());
const mediaById = new Map<string, MediaAsset>(
(mediaAssets ?? []).map((asset) => [asset.id, asset]),
);
const childAssetsByMindmapId: Record<string, MediaAsset[]> = {};
const childIds = new Set<string>();
const assigned = new Set<string>();
// 物理目录:storage_path 归属到 mindmaps/<mindmapId>/ 的附件,作为导图文件夹内容
(mediaAssets ?? []).forEach((asset) => {
const sp = asset.storage_path;
if (!sp || typeof sp !== "string") return;
const mindmapId = extractMindmapIdFromStoragePath(sp);
if (!mindmapId) return;
if (!mindmapIds.has(mindmapId)) return;
const docId = mindmapDocById.get(mindmapId);
if (docId && asset.document_id !== docId) return;
if (assigned.has(asset.id)) return;
assigned.add(asset.id);
childIds.add(asset.id);
if (!childAssetsByMindmapId[mindmapId]) childAssetsByMindmapId[mindmapId] = [];
childAssetsByMindmapId[mindmapId].push(asset);
});
// 引用图片:从 mindmap JSON 解析出的 assetIds,也放到导图文件夹下(去重)
(mindmapAssets ?? []).forEach((mindmapAsset) => {
const ids = mapping[mindmapAsset.id] ?? [];
if (!Array.isArray(ids) || ids.length === 0) return;
ids.forEach((id) => {
const asset = mediaById.get(id);
if (!asset) return;
if (asset.document_id !== mindmapAsset.document_id) return;
if (assigned.has(asset.id)) return;
assigned.add(asset.id);
childIds.add(asset.id);
if (!childAssetsByMindmapId[mindmapAsset.id]) childAssetsByMindmapId[mindmapAsset.id] = [];
childAssetsByMindmapId[mindmapAsset.id].push(asset);
});
});
return { childAssetsByMindmapId, childIds };
}, [mediaAssets, mindmapAssets, sidebarData.mindmapAssetChildren]);
const assetsByDoc = useMemo(() => {
const map: Record<string, MediaAsset[]> = {};
const assets = [...(mediaAssets ?? []), ...(mindmapAssets ?? []), ...(tableAssets ?? [])];
const assets = [
...((mediaAssets ?? []).filter(
(asset) => !mindmapChildrenSnapshot.childIds.has(asset.id),
)),
...(mindmapAssets ?? []),
...(tableAssets ?? []),
];
assets.forEach((asset) => {
if (!map[asset.document_id]) {
@@ -288,7 +369,9 @@ export function Sidebar({ initialData }: SidebarProps) {
}
});
return map;
}, [mediaAssets, mindmapAssets, tableAssets]);
}, [mediaAssets, mindmapAssets, tableAssets, mindmapChildrenSnapshot.childIds]);
const [expandedAssetFolders, setExpandedAssetFolders] = useState<Set<string>>(() => new Set());
const fileTreeRows = useMemo(
() =>
@@ -296,8 +379,10 @@ export function Sidebar({ initialData }: SidebarProps) {
nodes: filteredPrivateTree,
expanded,
assetsByDoc,
assetChildrenByAssetId: mindmapChildrenSnapshot.childAssetsByMindmapId,
expandedAssetFolderIds: expandedAssetFolders,
}),
[assetsByDoc, expanded, filteredPrivateTree],
[assetsByDoc, expanded, expandedAssetFolders, filteredPrivateTree, mindmapChildrenSnapshot.childAssetsByMindmapId],
);
const fileTreeVisibleRowIds = useMemo(() => fileTreeRows.map((row) => row.rowId), [fileTreeRows]);
@@ -406,6 +491,29 @@ export function Sidebar({ initialData }: SidebarProps) {
});
}, []);
const knownMindmapFolderIdsRef = useRef<Set<string>>(new Set());
useEffect(() => {
setExpandedAssetFolders((prev) => {
const next = new Set(prev);
(mindmapAssets ?? []).forEach((asset) => {
if (asset.asset_type !== "mindmap") return;
if (knownMindmapFolderIdsRef.current.has(asset.id)) return;
knownMindmapFolderIdsRef.current.add(asset.id);
next.add(asset.id);
});
return next;
});
}, [mindmapAssets]);
const toggleAssetFolderExpand = useCallback((assetId: string) => {
setExpandedAssetFolders((prev) => {
const next = new Set(prev);
if (next.has(assetId)) next.delete(assetId);
else next.add(assetId);
return next;
});
}, []);
const handleOpenAsset = useCallback((asset: MediaAsset) => {
if (asset.asset_type === "mindmap") {
router.push(`/mindmap/${asset.document_id}/${asset.id}`);
@@ -480,7 +588,7 @@ export function Sidebar({ initialData }: SidebarProps) {
const handleFileTreeRowDoubleClick = useCallback(
(row: FileTreeRow, _event?: React.MouseEvent) => {
if (row.kind === "asset") {
if (row.kind === "asset" || row.kind === "asset-folder") {
handleOpenAsset(row.asset);
return;
}
@@ -497,8 +605,8 @@ export function Sidebar({ initialData }: SidebarProps) {
reduceFileTreeSelection(prev, { type: "contextmenu", rowId: row.rowId }),
);
if (row.kind === "asset") {
setAssetMenu({ asset: row.asset, x: event.clientX, y: event.clientY });
if (row.kind === "asset" || row.kind === "asset-folder") {
setAssetMenu({ asset: row.asset, x: event.clientX, y: event.clientY });
return;
}
@@ -1052,11 +1160,26 @@ export function Sidebar({ initialData }: SidebarProps) {
);
const handleFileTreeDropFiles = useCallback(
(docId: string, files: FileList) => {
(docId: string, files: FileList, targetRow?: FileTreeRow) => {
void (async () => {
const droppedFiles = Array.from(files ?? []);
if (droppedFiles.length === 0) return;
const targetMindmapId = (() => {
if (!targetRow) return null;
if (targetRow.kind === "asset-folder" && targetRow.asset.asset_type === "mindmap") {
return targetRow.asset.id;
}
if (targetRow.kind === "asset") {
return extractMindmapIdFromStoragePath(targetRow.asset.storage_path);
}
return null;
})();
if (targetMindmapId) {
setExpandedAssetFolders((prev) => new Set(prev).add(targetMindmapId));
}
const inferredTargetDocId =
docId ||
inferPasteTargetDocId({
@@ -1085,6 +1208,9 @@ export function Sidebar({ initialData }: SidebarProps) {
form.append("file", file);
form.append("workspaceId", workspaceId);
form.append("documentId", inferredTargetDocId);
if (targetMindmapId) {
form.append("mindmapId", targetMindmapId);
}
const resp = await fetch("/api/media/upload", { method: "POST", body: form });
if (!resp.ok) {
const payload = await resp.json().catch(() => ({}));
@@ -1095,7 +1221,7 @@ export function Sidebar({ initialData }: SidebarProps) {
if (payload.asset?.id) {
emitAssetsChanged(inferredTargetDocId, payload.asset);
// 拖拽文件进文件树:如果落点就是当前打开的页面,则把文件也插入到主编辑区
if (inferredTargetDocId === activeId) {
if (inferredTargetDocId === activeId && !targetMindmapId) {
editorBridge?.insertMediaAsset?.(payload.asset);
}
} else {
@@ -1140,6 +1266,22 @@ export function Sidebar({ initialData }: SidebarProps) {
return;
}
const targetMindmapId = (() => {
if (args.targetRow.kind === "asset-folder" && args.targetRow.asset.asset_type === "mindmap") {
return args.targetRow.asset.id;
}
if (args.targetRow.kind === "asset") {
return extractMindmapIdFromStoragePath(args.targetRow.asset.storage_path);
}
return null;
})();
const targetSubPath = targetMindmapId ? `mindmaps/${targetMindmapId}` : undefined;
if (targetMindmapId) {
setExpandedAssetFolders((prev) => new Set(prev).add(targetMindmapId));
}
const uniqueRowIds: string[] = [];
const seen = new Set<string>();
args.rowIds.forEach((id) => {
@@ -1188,6 +1330,7 @@ export function Sidebar({ initialData }: SidebarProps) {
action: "copy",
assetIds: copyableAssetIds,
targetDocumentId: targetDocId,
targetSubPath,
}),
});
if (!resp.ok) {
@@ -1248,6 +1391,7 @@ export function Sidebar({ initialData }: SidebarProps) {
action: "move",
assetIds: copyableAssetIds,
targetDocumentId: targetDocId,
targetSubPath,
}),
});
if (!resp.ok) {
@@ -1533,9 +1677,20 @@ export function Sidebar({ initialData }: SidebarProps) {
openSearchPalette();
return;
}
if (
buttonId === "starred" ||
buttonId === "public" ||
buttonId === "shared" ||
buttonId === "templates"
) {
setViewMode("section");
setSectionsTrayOpen(true);
setSectionCollapsed(buttonId, false);
return;
}
window.alert("该功能即将上线,敬请期待");
},
[openSearchPalette],
[openSearchPalette, setSectionCollapsed, setSectionsTrayOpen, setViewMode],
);
useEffect(() => {
@@ -1666,34 +1821,55 @@ export function Sidebar({ initialData }: SidebarProps) {
{viewMode === "section" ? (
<>
<SectionList
label="星标置顶"
icon={SECTION_ICONS.starred}
nodes={starredNodes}
collapsed={collapsedSections.starred}
onToggle={() => toggleSection("starred")}
/>
<SectionList
label="公共页面"
icon={SECTION_ICONS.public}
nodes={publicNodes}
collapsed={collapsedSections.public}
onToggle={() => toggleSection("public")}
/>
<SectionList
label="共享页面"
icon={SECTION_ICONS.shared}
nodes={sharedNodes}
collapsed={collapsedSections.shared}
onToggle={() => toggleSection("shared")}
/>
<SectionList
label="模板中心"
icon={SECTION_ICONS.templates}
nodes={templateNodes}
collapsed={collapsedSections.templates}
onToggle={() => toggleSection("templates")}
/>
<button
type="button"
className="flex w-full items-center justify-between border-b border-[#f1f1f1] px-4 py-3 text-sm font-medium text-gray-600 hover:bg-gray-50"
onClick={toggleSectionsTray}
>
<span className="flex items-center gap-2">
<Star className="h-4 w-4 text-[#f5a623]" />
/ / /
</span>
<ChevronRight
className={cn(
"h-4 w-4 text-gray-400 transition-transform",
sectionsTrayOpen && "rotate-90",
)}
/>
</button>
{sectionsTrayOpen ? (
<>
<SectionList
label="星标置顶"
icon={SECTION_ICONS.starred}
nodes={starredNodes}
collapsed={collapsedSections.starred}
onToggle={() => toggleSection("starred")}
/>
<SectionList
label="公共页面"
icon={SECTION_ICONS.public}
nodes={publicNodes}
collapsed={collapsedSections.public}
onToggle={() => toggleSection("public")}
/>
<SectionList
label="共享页面"
icon={SECTION_ICONS.shared}
nodes={sharedNodes}
collapsed={collapsedSections.shared}
onToggle={() => toggleSection("shared")}
/>
<SectionList
label="模板中心"
icon={SECTION_ICONS.templates}
nodes={templateNodes}
collapsed={collapsedSections.templates}
onToggle={() => toggleSection("templates")}
/>
</>
) : null}
<div className="flex flex-1 flex-col border-t border-[#f1f1f1]">
<button
@@ -1740,6 +1916,7 @@ export function Sidebar({ initialData }: SidebarProps) {
onRowContextMenu={handleFileTreeRowContextMenu}
onRowDragStart={(row) => handleFileTreeRowDragStart(row)}
onToggleExpand={toggleExpand}
onToggleAssetFolderExpand={toggleAssetFolderExpand}
onCreateChild={handleCreate}
onBlankMouseDown={handleFileTreeBlankMouseDown}
onDropFiles={handleFileTreeDropFiles}