feat: unify workspace sidebar and create Foundry graph pages

This commit is contained in:
2026-11-05 23:57:45 +08:00
parent 6ca4b00d2c
commit 71bf8b0f5e
15 changed files with 417 additions and 238 deletions
+58
View File
@@ -0,0 +1,58 @@
"use client";
import { useEffect, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { Network } from "lucide-react";
import { api, errorText } from "@/lib/api";
import { readDemoBoards } from "@/lib/demo-workspace";
import type { Board } from "@/lib/workspace";
import styles from "./workspace.module.css";
export default function GraphPage({ workspaceId, boardId }: { workspaceId: string; boardId: string }) {
const demo = useSearchParams().get("demo") === "1";
const router = useRouter();
const [board, setBoard] = useState<Board | null>(null);
const [error, setError] = useState("");
useEffect(() => {
setBoard(null);
setError("");
if (demo) {
try {
const list = readDemoBoards(workspaceId);
const item = list.find((b) => b.id === boardId);
if (item) setBoard(item);
else setError("图谱不存在。");
} catch { setError("无法读取本地演示图谱。"); }
return;
}
let active = true;
const controller = new AbortController();
api<Board>(`workspaces/${encodeURIComponent(workspaceId)}/materials/${encodeURIComponent(boardId)}`, { signal: controller.signal })
.then((item) => {
if (!active) return;
if (item.kind !== "board" || item.workspace_id !== workspaceId) {
setError("当前页面不是此工作空间的图谱。");
return;
}
setBoard(item);
})
.catch((e) => {
if (!active) return;
setError(errorText(e));
if (e.message === "invalid_session") router.replace("/login");
});
return () => { active = false; controller.abort(); };
}, [workspaceId, boardId, demo, router]);
return (
<main className={styles.graph} aria-label="无限图谱">
{error ? <p className={styles.graphStatus} role="alert">{error}</p> : board ? (
<>
<header className={styles.graphHeader}><Network size={16} /><h1>{board.title}</h1>{demo && <span>演示</span>}</header>
<div className={styles.graphSurface} aria-label={board.title + "的空白图谱"} />
</>
) : <p className={styles.graphStatus} role="status">正在打开图谱…</p>}
</main>
);
}
@@ -0,0 +1,53 @@
.shell { display: flex; min-height: 100dvh; background: #fffefb; color: #55534f; }
.sidebar {
position: sticky; top: 0; display: flex; flex-direction: column;
width: 270px; max-width: calc(100vw - 40px); height: 100dvh; flex: 0 0 auto;
padding: 12px 10px 8px; background: #f8f7f5; border-right: 1px solid #edece9;
font-size: 13px; z-index: 30;
}
.topRow { display: flex; align-items: center; justify-content: space-between; margin: 0 0 12px; color: #85817b; }
.search { display: flex; align-items: center; gap: 7px; border: 1px solid #e6e3df; border-radius: 7px; padding: 7px 9px; color: #97938d; }
.search:focus-within { border-color: #b2a292; box-shadow: 0 0 0 2px #b2a29218; }
.search input { min-width: 0; width: 100%; padding: 0; background: transparent; border: 0; outline: none; font-size: 13px; color: #55534f; }
.search input::placeholder { color: #9a9690; }
.navigation { flex: 1; min-height: 0; overflow-y: auto; padding-top: 14px; }
.home, .pageLink { display: flex; align-items: center; gap: 9px; padding: 8px; min-height: 33px; border-radius: 6px; }
.home { width: fit-content; font-weight: 600; background: #ecebe8; }
.pageLink { margin: 2px 0; }
.pageLink svg { flex-shrink: 0; color: #96928b; }
.pageLink span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pageLink:hover, .pageLink[aria-current="page"] { background: #eae7e2; }
.sectionLabel { display: flex; justify-content: space-between; padding: 26px 7px 8px; color: #9b9690; font-size: 11px; }
.hint { padding: 6px 8px; color: #9a948c; line-height: 1.7; font-size: 12px; }
.error { margin: 6px 4px; font-size: 12px; color: #b14b3c; overflow-wrap: anywhere; }
.accountArea { margin-top: auto; padding-top: 10px; }
.accountButton { display: flex; align-items: center; gap: 9px; padding: 8px; width: 100%; border-radius: 7px; text-align: left; }
.accountButton:hover { background: #edeae5; }
.avatar { display: grid; place-items: center; flex-shrink: 0; width: 27px; height: 27px; border-radius: 7px; background: #e8e0d6; color: #705844; font-size: 13px; font-weight: 600; }
.accountText { display: flex; flex: 1; flex-direction: column; min-width: 0; gap: 2px; }
.accountText strong { font-size: 12px; font-weight: 500; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.accountText small { font-size: 10px; color: #969089; }
.accountPopup {
position: fixed; inset: auto auto 64px 8px; margin: 0; width: 300px; max-width: calc(100vw - 16px);
padding: 8px; color: #55534f; background: #fff; border: 1px solid #e9e5df;
border-radius: 10px; box-shadow: 0 8px 30px #38302612; font-size: 13px;
}
.accountPopup::backdrop { background: transparent; }
.accountHeading { display: flex; align-items: center; gap: 9px; padding: 7px 6px 12px; border-bottom: 1px solid #efede9; }
.accountHeading > div { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.accountHeading strong { font-size: 13px; font-weight: 600; }
.accountHeading small { color: #969089; font-size: 11px; overflow-wrap: anywhere; }
.popupLabel { padding: 12px 7px 5px; color: #9a948c; font-size: 11px; }
.menuItem { display: flex; align-items: center; gap: 8px; width: 100%; padding: 9px 7px; border-radius: 5px; text-align: left; }
.menuItem > span { flex: 1; }
.menuItem:hover { background: #f4f1ec; }
.separator { height: 1px; margin: 8px 0; background: #efede9; }
.floating { position: fixed; left: 12px; top: 12px; z-index: 40; display: flex; gap: 6px; padding: 6px; background: #fffefa; border: 1px solid #e7e2db; border-radius: 8px; box-shadow: 0 3px 14px #3c312b0d; color: #85817b; }
.floatingError { position: absolute; left: 0; top: 42px; width: 240px; padding: 10px; background: white; border: 1px solid #e7e2db; border-radius: 8px; color: #b14b3c; font-size: 12px; }
.graph { flex: 1; min-width: 0; display: flex; flex-direction: column; min-height: 100dvh; }
.graphHeader { display: flex; align-items: center; gap: 10px; padding: 20px 24px 20px 110px; min-height: 64px; border-bottom: 1px solid #efede8; font-size: 13px; }
.graphHeader h1 { margin: 0; font-size: 14px; font-weight: 500; }
.graphHeader span { color: #aaa299; }
.graphSurface { flex: 1; background-image: radial-gradient(#b8aea245 1px, transparent 1px); background-size: 22px 22px; }
.graphStatus { padding: 80px 24px; color: #9a948c; text-align: center; font-size: 13px; }
@media (max-width: 600px) { .sidebar { width: 240px; } .graphHeader { padding: 20px 14px; } }