diff --git a/backend/src/backend/scripts.py b/backend/src/backend/scripts.py index a5ea9c6..8673f39 100644 --- a/backend/src/backend/scripts.py +++ b/backend/src/backend/scripts.py @@ -1360,3 +1360,54 @@ async def version_download_url( request, ) return {"request_id": context.request_id, "data": data["data"], "meta": {}} + + +@router.get("/api/v1/scripts/{script_id}/content") +async def get_script_content( + script_id: str, + request: Request, + context: RequestContext = Depends(request_context), + session: AsyncSession = Depends(database_session), +) -> dict[str, Any]: + """获取脚本内容(用于只读预览)。 + + 适用于被锁定的脚本,非所有者只能查看内容,不能编辑。 + 该接口不检查锁状态,调用方需自行判断权限。 + """ + script = await session.scalar( + select(Scripts) + .where( + Scripts.script_id == script_id, + Scripts.workspace_id == context.workspace.workspace_id, + Scripts.status == "active", + ) + ) + if script is None: + raise HTTPException(status.HTTP_404_NOT_FOUND, "script not found") + + # 获取 Jupyter 路径 + jupyter_name = _jupyter_path(script.script_type, script.script_id) + + # 从 Jupyter 读取内容 + runtime_client = request.app.state.runtime_client + try: + content_data = await runtime_client.get_file( + context.workspace.workspace_id, + name=jupyter_name, + ) + except RuntimeClientError as exc: + raise HTTPException( + status_code=exc.status_code, + detail=exc.detail, + ) from exc + + return { + "request_id": context.request_id, + "data": { + "script_id": script.script_id, + "script_type": script.script_type, + "content": content_data.get("content"), + "format": content_data.get("format"), + }, + "meta": {}, + } diff --git a/frontend/app/features/platform/ScriptWorkspace.tsx b/frontend/app/features/platform/ScriptWorkspace.tsx index 1d68bcf..e566a9a 100644 --- a/frontend/app/features/platform/ScriptWorkspace.tsx +++ b/frontend/app/features/platform/ScriptWorkspace.tsx @@ -6,11 +6,13 @@ import type { ScriptType, } from "../../services/api"; import { scriptIcon } from "./WorkspaceTree"; -import type { MouseEvent as ReactMouseEvent, RefObject } from "react"; +import type { MouseEvent as ReactMouseEvent } from "react"; import Editor from "@monaco-editor/react"; -import { useRef, useLayoutEffect } from "react"; +import { useRef, useLayoutEffect, useState, useEffect } from "react"; import { useScriptWorkspaceStore } from "./state/scriptWorkspaceStore"; +import { useAuth } from "../../context/AuthContext"; +import { getScriptContent } from "../../services/api"; type ToastState = { tone: "success" | "error" | "info"; @@ -118,12 +120,23 @@ export function ScriptWorkspace({ onPublish, onInfo, }: ScriptWorkspaceProps) { + const { user } = useAuth(); const tabbarRef = useRef(null); const isNotebook = script.script_type === "notebook"; // 判断当前选中的文件是否正在编辑(需要同时检查 session_status 和 script_id) const isEditing = editSession?.session_status === "active" && editSession?.script_id === script.script_id; + // 只读模式状态 + 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; @@ -147,6 +160,36 @@ export function ScriptWorkspace({ } }, [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 ( <>
@@ -244,165 +287,203 @@ export function ScriptWorkspace({
- {/* 多 iframe 共存方案:为每个缓存的 session 渲染独立的 iframe */} -
0 ? 'is-embedded' : ''}`} style={sessionCache.size > 0 ? { position: 'relative', minHeight: '0', height: '100%' } : undefined}> - {/* 渲染所有缓存的 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)} - -
-