import Icon from "../../components/common/Icon"; import type { ActiveEditSession, LatestVersion, ScriptItem, ScriptType, } from "~/services/api"; import { scriptIcon } from "./WorkspaceTree"; import type { MouseEvent as ReactMouseEvent } from "react"; import Editor from "@monaco-editor/react"; import { useRef, useLayoutEffect, useState, useEffect } from "react"; import { useScriptWorkspaceStore, type PythonEditorBuffer } from "./state/scriptWorkspaceStore"; import { PythonEditor } from "./PythonEditor"; import { useAuth } from "~/context/AuthContext"; import { getScriptContent } from "~/services/api"; type ToastState = { tone: "success" | "error" | "info"; message: string; }; // 缓存的会话类型(多 iframe 共存方案) interface CachedSession { session: ActiveEditSession; jupyterUrl: string; lastActiveTime: number; } type ScriptWorkspaceProps = { script: ScriptItem; scripts: ScriptItem[]; sessionCache: Map; editSession: ActiveEditSession | null; jupyterUrl: string | null; editBusy: boolean; openError: string | null; latestVersion: LatestVersion | null; versionsLoading: boolean; openTabs: Array<{ scriptId: string; scriptName: string; scriptType: ScriptType }>; onOpenEditor: () => void; onEndEditing: () => void; onClose: (scriptId: string, event?: ReactMouseEvent) => void; onSwitchTab: (scriptId: string) => void; onNewTab: () => void; onPublish: () => void; onInfo: (toast: ToastState) => void; pythonEditorBuffers: Record; onOpenPythonEditor: () => void; onSetPythonEditorContent: (scriptId: string, v: string) => void; onSavePythonEditor: (scriptId: string) => void; onExitPythonEditor: (scriptId: string) => void; onClosePythonTab: (scriptId: string) => void; }; function formatTime(value: string) { return new Intl.DateTimeFormat("zh-CN", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", hour12: false, }).format(new Date(value)); } function formatBytes(value: number) { if (value < 1024) return `${value} B`; return `${(value / 1024).toFixed(1)} KB`; } function shortHash(value: string) { return value ? `${value.slice(0, 8)}…${value.slice(-6)}` : "—"; } function confineJupyterFrame(frame: HTMLIFrameElement): void { try { const document = frame.contentDocument; if (!document?.documentElement) return; const keepInside = (): void => { document.querySelectorAll("button,[role='button']").forEach( (element) => { const label = `${element.getAttribute("aria-label") ?? ""} ${ element.getAttribute("title") ?? "" } ${element.textContent ?? ""}`.trim(); if (/^open in\b/i.test(label) || /\bopen in\.\.\./i.test(label)) { element.style.setProperty("display", "none", "important"); } }, ); document.querySelectorAll("a[target]").forEach((link) => { if (["_blank", "_top", "_parent"].includes(link.target)) { link.target = "_self"; } }); }; keepInside(); new MutationObserver(keepInside).observe(document.documentElement, { childList: true, subtree: true, }); document.addEventListener("click", (event) => { const target = event.target as HTMLElement | null; const link = target?.closest?.("a") as HTMLAnchorElement | null; if (link && ["_blank", "_top", "_parent"].includes(link.target)) { link.target = "_self"; } }, true); } catch { // The iframe remains sandboxed even if its document is not yet accessible. } } export function ScriptWorkspace({ script, scripts, sessionCache, editSession, jupyterUrl, editBusy, openError, latestVersion, versionsLoading, openTabs, onOpenEditor, onEndEditing, onClose, onSwitchTab, onNewTab, onPublish, onInfo, pythonEditorBuffers, onOpenPythonEditor, onSetPythonEditorContent, onSavePythonEditor, onExitPythonEditor, onClosePythonTab, }: ScriptWorkspaceProps) { const { user } = useAuth(); const tabbarRef = useRef(null); const isNotebook = script.script_type === "notebook"; const isPython = script.script_type === "python"; // 判断当前选中的文件是否正在编辑(需要同时检查 session_status 和 script_id) const isEditing = editSession?.session_status === "active" && editSession?.script_id === script.script_id; const activePythonBuf = isPython ? pythonEditorBuffers[script.script_id] : null; const isPythonEditing = isPython && !!activePythonBuf; const showSaveButton = isPythonEditing && activePythonBuf && (activePythonBuf.dirty || activePythonBuf.saving || activePythonBuf.initial); const saveDisabled = !activePythonBuf?.dirty || activePythonBuf?.saving; const pythonTabBuffers = openTabs.filter( (t) => t.scriptType === "python" && pythonEditorBuffers[t.scriptId], ); const hasEmbeddedContent = sessionCache.size > 0 || pythonTabBuffers.length > 0; // 只读模式状态 const [readOnlyContent, setReadOnlyContent] = useState(null); const [readOnlyLoading, setReadOnlyLoading] = useState(false); const [readOnlyError, setReadOnlyError] = useState(null); // 判断是否启用只读模式:文件已锁定 + 非所有者 + 非管理员 const isReadOnlyMode = script.is_locked && user?.user_id !== script.owner_user_id && user?.role_code !== "admin"; const scroll = (direction: "left" | "right") => { const tabbar = tabbarRef.current; if (!tabbar) return; const scrollAmount = 200; tabbar.scrollBy({ left: direction === "left" ? -scrollAmount : scrollAmount, behavior: "smooth", }); }; useLayoutEffect(() => { const tabbar = tabbarRef.current; if (!tabbar) return; const activeTab = tabbar.querySelector(".editor-tab--active") as HTMLElement | null; if (activeTab) { const tabbarRect = tabbar.getBoundingClientRect(); const tabRect = activeTab.getBoundingClientRect(); if (tabRect.right > tabbarRect.right || tabRect.left < tabbarRect.left) { activeTab.scrollIntoView({ behavior: "smooth", inline: "center" }); } } }, [script.script_id]); // 加载只读内容(当处于只读模式时) useEffect(() => { if (!isReadOnlyMode) { setReadOnlyContent(null); setReadOnlyLoading(false); setReadOnlyError(null); return; } setReadOnlyLoading(true); setReadOnlyError(null); getScriptContent(script.workspace_id, script.script_id) .then((data) => { if (data.script_type === 'notebook') { // Notebook 转为 JSON 字符串显示 setReadOnlyContent(JSON.stringify(data.content, null, 2)); } else { // Python 文件直接显示 setReadOnlyContent(data.content as string); } }) .catch((err) => { setReadOnlyError(err instanceof Error ? err.message : '加载失败'); }) .finally(() => { setReadOnlyLoading(false); }); }, [isReadOnlyMode, script.script_id, script.workspace_id, script.script_type]); return ( <>
{openTabs.map((tab) => (
onSwitchTab(tab.scriptId)} > {tab.scriptName}
))}
工作副本 {script.script_name}
{isPythonEditing ? ( <> {showSaveButton && ( )} ) : isEditing ? ( ) : ( )} { latestVersion ? `最新 ${latestVersion.version_label}` : "工作副本已就绪" }
{/* 只读模式:显示只读编辑器 */} {isReadOnlyMode ? (
此文件已被锁定,您当前处于只读模式
{readOnlyLoading ? (
加载内容中...
) : readOnlyError ? (
加载失败

{readOnlyError}

) : ( )}
) : ( // 正常编辑模式:原有的 iframe + Monaco 预览逻辑
0 ? 'is-embedded' : ''}`} style={sessionCache.size > 0 ? { position: 'relative', minHeight: '0', height: '100%' } : undefined}> {/* 多 PythonEditor 实例:每个有 buffer 的 python tab 都挂载,仅 active 可见 */} {pythonTabBuffers.map((tab) => { const tabScript = scripts.find((s) => s.script_id === tab.scriptId); if (!tabScript) return null; const buf = pythonEditorBuffers[tab.scriptId]; const isActive = tab.scriptId === script.script_id; return (
); })} {/* 渲染所有缓存的 iframe */} {Array.from(sessionCache.entries()).map(([scriptId, cached]) => { const isActive = scriptId === script.script_id; return (
Workspace Jupyter Server {cached.session.session_status === "active" ? "Notebook Session · Python 3 Kernel" : "Jupyter 文本编辑器"} Runtime {cached.session.runtime_id.slice(-8)}