feat(ai-writing): 课题研究笔记本内嵌打开 + 新建/所有空间入口(0616 #5)
- 左栏点笔记本/所有空间 → 右侧内嵌打开(不跳路由),写作布局保持挂载(display:none,不丢 setup 线程),顶部「返回写作」 - ResearchSidebar 加「新建空间」(弹窗 createSearchSpace) 与「所有空间」入口 - StudioNotebookDetailPage 加可选 notebookId prop;StudioNotebooksPage 加可选 onOpenNotebook(内嵌时卡片走回调而非 Link),列表内点空间内嵌打开详情 - 内嵌笔记本详情用 ChatRuntime 包裹(修复 useArtifacts must be used within an ArtifactsProvider) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
f39cffcb9a
commit
0e4caa5a66
4 files changed
+243
-47
No files matched your search
@@ -1,8 +1,18 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { BookOpen, FileText } from "lucide-react";
|
import { BookOpen, FileText, LayoutGrid, Loader2, Plus } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { getStoredAuth } from "@/core/auth";
|
||||||
|
import { createSearchSpace } from "@/core/studio/api";
|
||||||
import { useStudioNotebooks } from "@/core/studio/hooks";
|
import { useStudioNotebooks } from "@/core/studio/hooks";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
@@ -16,38 +26,64 @@ import { useWritingForm } from "../../contexts/WritingFormContext";
|
|||||||
* - 下半「笔记本」= 我的空间(``useStudioNotebooks``);点击设为当前课题研究的素材来源
|
* - 下半「笔记本」= 我的空间(``useStudioNotebooks``);点击设为当前课题研究的素材来源
|
||||||
* (materialSource='notebook' + 选中该本)。
|
* (materialSource='notebook' + 选中该本)。
|
||||||
*/
|
*/
|
||||||
export function ResearchSidebar() {
|
export function ResearchSidebar({
|
||||||
|
viewingNotebookId,
|
||||||
|
onOpenNotebook,
|
||||||
|
onOpenAllSpaces,
|
||||||
|
}: {
|
||||||
|
/** 当前在右侧查看的笔记本 id(高亮用)。 */
|
||||||
|
viewingNotebookId?: number | null;
|
||||||
|
/** 点击笔记本 → 在右侧内嵌打开该笔记本页(替换右侧内容)。 */
|
||||||
|
onOpenNotebook: (notebookId: number) => void;
|
||||||
|
/** 点击「所有空间」→ 在右侧内嵌打开空间列表页。 */
|
||||||
|
onOpenAllSpaces: () => void;
|
||||||
|
}) {
|
||||||
const { articleTypes, refresh } = useArticleTypes();
|
const { articleTypes, refresh } = useArticleTypes();
|
||||||
const { notebooks } = useStudioNotebooks();
|
|
||||||
const {
|
const {
|
||||||
articleType,
|
notebooks,
|
||||||
setArticleType,
|
isLoading: notebooksLoading,
|
||||||
setWordCountInput,
|
isError: notebooksError,
|
||||||
setFormExpanded,
|
} = useStudioNotebooks();
|
||||||
materialSource,
|
const { articleType, setArticleType, setWordCountInput, setFormExpanded } =
|
||||||
setMaterialSource,
|
useWritingForm();
|
||||||
selectedNotebookId,
|
const queryClient = useQueryClient();
|
||||||
setSelectedNotebookId,
|
|
||||||
setSelectedDocumentIds,
|
// 新建空间弹窗(与 workspace-nav-studio 同款)。
|
||||||
} = useWritingForm();
|
const [createOpen, setCreateOpen] = useState(false);
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
const [creating, setCreating] = useState(false);
|
||||||
|
const [createError, setCreateError] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void refresh();
|
void refresh();
|
||||||
}, [refresh]);
|
}, [refresh]);
|
||||||
|
|
||||||
|
const handleCreate = async () => {
|
||||||
|
const trimmed = name.trim();
|
||||||
|
if (!trimmed) return;
|
||||||
|
setCreating(true);
|
||||||
|
setCreateError("");
|
||||||
|
try {
|
||||||
|
const email = getStoredAuth()?.email ?? "";
|
||||||
|
const token = queryClient.getQueryData<string>(["studio", "token", email]);
|
||||||
|
if (!token) throw new Error("未登录或 token 已过期,请刷新页面");
|
||||||
|
await createSearchSpace(trimmed, token);
|
||||||
|
setCreateOpen(false);
|
||||||
|
setName("");
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ["studio", "notebooks"] });
|
||||||
|
} catch (e) {
|
||||||
|
setCreateError(e instanceof Error ? e.message : "创建失败");
|
||||||
|
} finally {
|
||||||
|
setCreating(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleTemplateClick = (key: string, defaultWordCount: number) => {
|
const handleTemplateClick = (key: string, defaultWordCount: number) => {
|
||||||
setArticleType(key);
|
setArticleType(key);
|
||||||
if (defaultWordCount > 0) setWordCountInput(String(defaultWordCount));
|
if (defaultWordCount > 0) setWordCountInput(String(defaultWordCount));
|
||||||
setFormExpanded(true);
|
setFormExpanded(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleNotebookClick = (notebookId: number) => {
|
|
||||||
setMaterialSource("notebook");
|
|
||||||
setSelectedNotebookId(notebookId);
|
|
||||||
setSelectedDocumentIds([]);
|
|
||||||
setFormExpanded(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="flex h-full w-64 shrink-0 flex-col overflow-hidden border-r border-border/70 bg-card/60 backdrop-blur-sm">
|
<aside className="flex h-full w-64 shrink-0 flex-col overflow-hidden border-r border-border/70 bg-card/60 backdrop-blur-sm">
|
||||||
{/* 上半:研究模板(文章类型) */}
|
{/* 上半:研究模板(文章类型) */}
|
||||||
@@ -95,18 +131,37 @@ export function ResearchSidebar() {
|
|||||||
<h2 className="text-muted-foreground/70 px-2 mb-1.5 text-[10px] font-semibold tracking-[0.12em] uppercase select-none">
|
<h2 className="text-muted-foreground/70 px-2 mb-1.5 text-[10px] font-semibold tracking-[0.12em] uppercase select-none">
|
||||||
笔记本
|
笔记本
|
||||||
</h2>
|
</h2>
|
||||||
{notebooks.length === 0 ? (
|
<div className="flex flex-col gap-0.5">
|
||||||
<p className="px-2 py-1.5 text-xs text-muted-foreground/70">暂无笔记本</p>
|
{/* 新建空间 */}
|
||||||
) : (
|
<button
|
||||||
<div className="flex flex-col gap-0.5">
|
type="button"
|
||||||
{notebooks.map((nb) => {
|
onClick={() => {
|
||||||
const active =
|
setName("");
|
||||||
materialSource === "notebook" && selectedNotebookId === nb.id;
|
setCreateError("");
|
||||||
|
setCreateOpen(true);
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-2 rounded-lg px-2.5 py-1.5 text-left text-[13px] text-muted-foreground transition-colors hover:bg-sidebar-accent/60 hover:text-foreground"
|
||||||
|
>
|
||||||
|
<Plus className="size-3.5 shrink-0 text-muted-foreground/70" />
|
||||||
|
<span>新建空间</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{notebooksLoading ? (
|
||||||
|
<p className="px-2 py-1.5 text-xs text-muted-foreground/70">加载中…</p>
|
||||||
|
) : notebooksError ? (
|
||||||
|
<p className="px-2 py-1.5 text-xs text-amber-600">
|
||||||
|
笔记本服务暂不可用(检查空间服务地址 / 账号)
|
||||||
|
</p>
|
||||||
|
) : notebooks.length === 0 ? (
|
||||||
|
<p className="px-2 py-1.5 text-xs text-muted-foreground/70">暂无笔记本</p>
|
||||||
|
) : (
|
||||||
|
notebooks.map((nb) => {
|
||||||
|
const active = viewingNotebookId === nb.id;
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={nb.id}
|
key={nb.id}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleNotebookClick(nb.id)}
|
onClick={() => onOpenNotebook(nb.id)}
|
||||||
title={nb.description || nb.name}
|
title={nb.description || nb.name}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-2 rounded-lg px-2.5 py-1.5 text-left text-[13px] transition-colors",
|
"flex items-center gap-2 rounded-lg px-2.5 py-1.5 text-left text-[13px] transition-colors",
|
||||||
@@ -119,10 +174,60 @@ export function ResearchSidebar() {
|
|||||||
<span className="truncate">{nb.name}</span>
|
<span className="truncate">{nb.name}</span>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})
|
||||||
</div>
|
)}
|
||||||
)}
|
|
||||||
|
{/* 所有空间 */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onOpenAllSpaces}
|
||||||
|
className="flex items-center gap-2 rounded-lg px-2.5 py-1.5 text-left text-[13px] text-muted-foreground transition-colors hover:bg-sidebar-accent/60 hover:text-foreground"
|
||||||
|
>
|
||||||
|
<LayoutGrid className="size-3.5 shrink-0 text-muted-foreground/70" />
|
||||||
|
<span>所有空间</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 新建空间弹窗 */}
|
||||||
|
<Dialog open={createOpen} onOpenChange={(v) => !creating && setCreateOpen(v)}>
|
||||||
|
<DialogContent className="sm:max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>新建空间</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="flex flex-col gap-4 pt-2">
|
||||||
|
<input
|
||||||
|
autoFocus
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") void handleCreate();
|
||||||
|
}}
|
||||||
|
placeholder="空间名称"
|
||||||
|
disabled={creating}
|
||||||
|
className="w-full rounded-lg border border-border bg-background px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-primary disabled:opacity-50"
|
||||||
|
/>
|
||||||
|
{createError && <p className="text-xs text-destructive">{createError}</p>}
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
disabled={creating}
|
||||||
|
onClick={() => setCreateOpen(false)}
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
disabled={!name.trim() || creating}
|
||||||
|
onClick={() => void handleCreate()}
|
||||||
|
>
|
||||||
|
{creating ? <Loader2 className="size-4 animate-spin" /> : "创建"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -81,8 +81,8 @@
|
|||||||
* -d '{"retention_days": 0}'
|
* -d '{"retention_days": 0}'
|
||||||
* ──────────────────────────────────────────────────────────────────────────
|
* ──────────────────────────────────────────────────────────────────────────
|
||||||
*/
|
*/
|
||||||
import { useRef } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import { PanelRightOpen } from 'lucide-react';
|
import { ArrowLeft, PanelRightOpen } from 'lucide-react';
|
||||||
import { TooltipProvider } from '@/components/ui/tooltip';
|
import { TooltipProvider } from '@/components/ui/tooltip';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import type { GroupImperativeHandle } from 'react-resizable-panels';
|
import type { GroupImperativeHandle } from 'react-resizable-panels';
|
||||||
@@ -94,6 +94,12 @@ import { AIWritingPanel } from '../components/ai-writing/AIWritingPanel';
|
|||||||
import { AIWritingDraftPanel } from '../components/ai-writing/AIWritingDraftPanel';
|
import { AIWritingDraftPanel } from '../components/ai-writing/AIWritingDraftPanel';
|
||||||
import { WritingSetupChat } from '../components/ai-writing/WritingSetupChat';
|
import { WritingSetupChat } from '../components/ai-writing/WritingSetupChat';
|
||||||
import { ResearchSidebar } from '../components/ai-writing/ResearchSidebar';
|
import { ResearchSidebar } from '../components/ai-writing/ResearchSidebar';
|
||||||
|
import StudioNotebookDetailPage from '@/pages/StudioNotebookDetailPage';
|
||||||
|
import StudioNotebooksPage from '@/pages/StudioNotebooksPage';
|
||||||
|
import { ChatRuntime } from '@/pages/WorkspaceRoutes';
|
||||||
|
|
||||||
|
/** 右侧内嵌视图:笔记本详情 / 所有空间列表 / null(写作) */
|
||||||
|
type RightView = { kind: 'notebook'; id: number } | { kind: 'allSpaces' } | null;
|
||||||
import { AIWritingProvider, useAIWriting } from '../contexts/AIWritingContext';
|
import { AIWritingProvider, useAIWriting } from '../contexts/AIWritingContext';
|
||||||
import { AIWritingHistoryProvider, useAIWritingHistory } from '../contexts/AIWritingHistoryContext';
|
import { AIWritingHistoryProvider, useAIWritingHistory } from '../contexts/AIWritingHistoryContext';
|
||||||
import { WritingFormProvider, useWritingForm } from '../contexts/WritingFormContext';
|
import { WritingFormProvider, useWritingForm } from '../contexts/WritingFormContext';
|
||||||
@@ -222,6 +228,12 @@ export default function AIWritingPage({
|
|||||||
initialDocumentIds?: number[];
|
initialDocumentIds?: number[];
|
||||||
studioToken?: string;
|
studioToken?: string;
|
||||||
} = {}) {
|
} = {}) {
|
||||||
|
// 需求 0616 #5:左栏点「笔记本 / 所有空间」→ 在右侧内嵌打开对应页,替换右侧内容
|
||||||
|
// (写作布局不卸载,仅 display:none 隐藏,避免 WritingSetupChat 重挂丢线程)。
|
||||||
|
const [rightView, setRightView] = useState<RightView>(null);
|
||||||
|
const overlayActive = rightView != null;
|
||||||
|
const viewingNotebookId = rightView?.kind === 'notebook' ? rightView.id : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CanvasThreadProvider>
|
<CanvasThreadProvider>
|
||||||
<CanvasGraphProvider>
|
<CanvasGraphProvider>
|
||||||
@@ -237,9 +249,56 @@ export default function AIWritingPage({
|
|||||||
<div className="h-full flex flex-col overflow-hidden bg-background canvas-writing-bg">
|
<div className="h-full flex flex-col overflow-hidden bg-background canvas-writing-bg">
|
||||||
<div className="flex-1 min-h-0 overflow-hidden flex">
|
<div className="flex-1 min-h-0 overflow-hidden flex">
|
||||||
{/* 需求 0616 #5:课题研究左栏(研究模板 + 笔记本)。studio 嵌入态(studioToken)隐藏。 */}
|
{/* 需求 0616 #5:课题研究左栏(研究模板 + 笔记本)。studio 嵌入态(studioToken)隐藏。 */}
|
||||||
{!studioToken && <ResearchSidebar />}
|
{!studioToken && (
|
||||||
<div className="flex-1 min-h-0 overflow-hidden">
|
<ResearchSidebar
|
||||||
<AIWritingLayout hideAdvancedFields={hideAdvancedFields} initialNotebookId={initialNotebookId} initialDocumentIds={initialDocumentIds} studioToken={studioToken} />
|
viewingNotebookId={viewingNotebookId}
|
||||||
|
onOpenNotebook={(id) => setRightView({ kind: 'notebook', id })}
|
||||||
|
onOpenAllSpaces={() => setRightView({ kind: 'allSpaces' })}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{/* 右侧:写作布局常驻(仅 display:none 隐藏,避免 WritingSetupChat 重挂丢线程),
|
||||||
|
查看笔记本 / 所有空间时在其上叠加内嵌页。 */}
|
||||||
|
<div className="relative flex-1 min-h-0 overflow-hidden">
|
||||||
|
<div className={cn('absolute inset-0', overlayActive && 'hidden')}>
|
||||||
|
<AIWritingLayout hideAdvancedFields={hideAdvancedFields} initialNotebookId={initialNotebookId} initialDocumentIds={initialDocumentIds} studioToken={studioToken} />
|
||||||
|
</div>
|
||||||
|
{overlayActive && (
|
||||||
|
<div className="absolute inset-0 flex flex-col bg-background">
|
||||||
|
<div className="flex shrink-0 items-center gap-1 border-b border-border px-4 py-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setRightView(null)}
|
||||||
|
className="flex items-center gap-1.5 rounded-md px-2 py-1 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="size-4" />
|
||||||
|
返回写作
|
||||||
|
</button>
|
||||||
|
{rightView.kind === 'notebook' && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setRightView({ kind: 'allSpaces' })}
|
||||||
|
className="ml-1 rounded-md px-2 py-1 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
||||||
|
>
|
||||||
|
所有空间
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="min-h-0 flex-1 overflow-hidden">
|
||||||
|
{rightView.kind === 'notebook' ? (
|
||||||
|
// 笔记本详情页内部用 ChatBox(useArtifacts) → 需 ChatRuntime 提供 Provider。
|
||||||
|
<ChatRuntime>
|
||||||
|
<StudioNotebookDetailPage key={rightView.id} notebookId={rightView.id} />
|
||||||
|
</ChatRuntime>
|
||||||
|
) : (
|
||||||
|
<div className="h-full overflow-y-auto">
|
||||||
|
<StudioNotebooksPage
|
||||||
|
onOpenNotebook={(id) => setRightView({ kind: 'notebook', id })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -209,8 +209,15 @@ function SourcesDialog({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── 主页面 ──────────────────────────────────────────────
|
// ── 主页面 ──────────────────────────────────────────────
|
||||||
export default function StudioNotebookDetailPage() {
|
export default function StudioNotebookDetailPage({
|
||||||
const { notebookId } = useParams<{ notebookId: string }>();
|
notebookId: notebookIdProp,
|
||||||
|
}: {
|
||||||
|
/** 内嵌使用(如课题研究右侧)时直接传入;缺省则从路由参数取。 */
|
||||||
|
notebookId?: number;
|
||||||
|
} = {}) {
|
||||||
|
const params = useParams<{ notebookId: string }>();
|
||||||
|
const notebookId =
|
||||||
|
notebookIdProp != null ? String(notebookIdProp) : params.notebookId;
|
||||||
const { notebooks, isLoading, token: studioToken } = useStudioNotebooks(); // token 用于跳转 callback
|
const { notebooks, isLoading, token: studioToken } = useStudioNotebooks(); // token 用于跳转 callback
|
||||||
const { models } = useModels();
|
const { models } = useModels();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|||||||
@@ -39,17 +39,16 @@ function NotebookCard({
|
|||||||
nb,
|
nb,
|
||||||
onRename,
|
onRename,
|
||||||
onDelete,
|
onDelete,
|
||||||
|
onOpen,
|
||||||
}: {
|
}: {
|
||||||
nb: StudioNotebook;
|
nb: StudioNotebook;
|
||||||
onRename: (nb: StudioNotebook) => void;
|
onRename: (nb: StudioNotebook) => void;
|
||||||
onDelete: (nb: StudioNotebook) => void;
|
onDelete: (nb: StudioNotebook) => void;
|
||||||
|
/** 传入则点击卡片回调(内嵌打开),不再路由跳转。 */
|
||||||
|
onOpen?: (id: number) => void;
|
||||||
}) {
|
}) {
|
||||||
return (
|
const inner = (
|
||||||
<div className="group relative flex flex-col rounded-[20px] bg-[#f2f2f2] transition-colors hover:bg-[#e8e8e8]" style={{ minHeight: 180 }}>
|
<>
|
||||||
<Link
|
|
||||||
to={`/page/workspace/studio/notebooks/${nb.id}`}
|
|
||||||
className="flex flex-1 flex-col p-5"
|
|
||||||
>
|
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
<div className="flex size-11 items-center justify-center text-3xl leading-none select-none">
|
<div className="flex size-11 items-center justify-center text-3xl leading-none select-none">
|
||||||
{nb.avatar_url ? (
|
{nb.avatar_url ? (
|
||||||
@@ -67,7 +66,27 @@ function NotebookCard({
|
|||||||
</span>
|
</span>
|
||||||
<span className="text-[13px] text-[#666]">{nb.source_count} 个来源</span>
|
<span className="text-[13px] text-[#666]">{nb.source_count} 个来源</span>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="group relative flex flex-col rounded-[20px] bg-[#f2f2f2] transition-colors hover:bg-[#e8e8e8]" style={{ minHeight: 180 }}>
|
||||||
|
{onOpen ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onOpen(nb.id)}
|
||||||
|
className="flex flex-1 flex-col p-5 text-left"
|
||||||
|
>
|
||||||
|
{inner}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<Link
|
||||||
|
to={`/page/workspace/studio/notebooks/${nb.id}`}
|
||||||
|
className="flex flex-1 flex-col p-5"
|
||||||
|
>
|
||||||
|
{inner}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* three-dot menu — absolute so it doesn't shift layout */}
|
{/* three-dot menu — absolute so it doesn't shift layout */}
|
||||||
<div className="absolute right-4 top-4">
|
<div className="absolute right-4 top-4">
|
||||||
@@ -99,7 +118,12 @@ function NotebookCard({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function StudioNotebooksPage() {
|
export default function StudioNotebooksPage({
|
||||||
|
onOpenNotebook,
|
||||||
|
}: {
|
||||||
|
/** 内嵌使用(课题研究右侧)时传入:点击空间卡片回调,不跳路由。 */
|
||||||
|
onOpenNotebook?: (id: number) => void;
|
||||||
|
} = {}) {
|
||||||
const { notebooks, isLoading, isError } = useStudioNotebooks();
|
const { notebooks, isLoading, isError } = useStudioNotebooks();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
@@ -204,6 +228,7 @@ export default function StudioNotebooksPage() {
|
|||||||
nb={nb}
|
nb={nb}
|
||||||
onRename={(nb) => { setRenameTarget(nb); setRenameName(nb.name); setRenameError(""); }}
|
onRename={(nb) => { setRenameTarget(nb); setRenameName(nb.name); setRenameError(""); }}
|
||||||
onDelete={(nb) => { setDeleteTarget(nb); setDeleteError(""); }}
|
onDelete={(nb) => { setDeleteTarget(nb); setDeleteError(""); }}
|
||||||
|
onOpen={onOpenNotebook}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in new issue
Block a user