feat:脚本只读

This commit is contained in:
xiaozhu
2026-08-12 14:34:31 +08:00
parent dc4d190935
commit fe65034aa8
4 changed files with 364 additions and 157 deletions
+238 -157
View File
@@ -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<HTMLDivElement | null>(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<string | null>(null);
const [readOnlyLoading, setReadOnlyLoading] = useState(false);
const [readOnlyError, setReadOnlyError] = useState<string | null>(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 (
<>
<div className="tabbar">
@@ -244,165 +287,203 @@ export function ScriptWorkspace({
</div>
</div>
{/* 多 iframe 共存方案:为每个缓存的 session 渲染独立的 iframe */}
<div className={`editor-canvas ${sessionCache.size > 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 (
<section
key={scriptId}
className="embedded-jupyter"
style={{
position: isActive ? 'relative' : 'absolute',
visibility: isActive ? 'visible' : 'hidden',
pointerEvents: isActive ? 'auto' : 'none',
width: '100%',
height: '100%',
overflow: 'hidden'
}}
>
<div className="embedded-jupyter__status">
<span>
<i />
Workspace Jupyter Server
</span>
<span>
{cached.session.session_status === "active" ? "Notebook Session · Python 3 Kernel" : "Jupyter 文本编辑器"}
</span>
<code title={cached.session.runtime_id}>
Runtime {cached.session.runtime_id.slice(-8)}
</code>
</div>
<iframe
src={cached.jupyterUrl}
title={`${scriptId} Jupyter 编辑器`}
allow="clipboard-read; clipboard-write"
sandbox="allow-same-origin allow-scripts allow-forms allow-downloads allow-modals"
onLoad={(event) => confineJupyterFrame(event.currentTarget)}
/>
</section>
);
})}
{/* 当前脚本没有缓存时的状态显示 */}
{!sessionCache.has(script.script_id) ? (
isNotebook ? (
<section
className={`editor-opening-state${openError ? " has-error" : ""}`}
aria-live="polite"
style={{ display: 'block' }}
>
<div className="editor-opening-state__icon">
{openError
? <Icon name="info" size={28} />
: <span className="button-spinner button-spinner--blue" />}
</div>
<strong>
{openError ? "Notebook 打开失败" : "正在打开 Notebook"}
</strong>
<p>
{openError
? openError
: "正在获取编辑锁并连接 Workspace Jupyter Server…"}
</p>
{openError && (
<button
className="open-editor-button"
type="button"
disabled={editBusy}
onClick={onOpenEditor}
>
{editBusy
? <span className="button-spinner button-spinner--blue" />
: <Icon name="refresh" size={16} />}
{editBusy ? "正在重试…" : "重试打开"}
</button>
)}
</section>
{/* 只读模式:显示只读编辑器 */}
{isReadOnlyMode ? (
<div className="readonly-editor-container">
<div className="readonly-editor-banner">
<Icon name="lock" size={16} />
<span></span>
</div>
{readOnlyLoading ? (
<div className="readonly-editor-loading">
<span className="button-spinner button-spinner--blue" />
<span>...</span>
</div>
) : readOnlyError ? (
<div className="readonly-editor-error">
<Icon name="info" size={28} />
<strong></strong>
<p>{readOnlyError}</p>
</div>
) : (
<section className="script-overview">
<div className="script-overview__header">
<div>
<span className="section-kicker">PYTHON SCRIPT</span>
<h2>{script.script_name}</h2>
<p>{script.relative_path}</p>
</div>
<button
className={`open-editor-button${isEditing ? " is-editing" : ""}`}
type="button"
disabled={editBusy}
onClick={onOpenEditor}
<Editor
height="calc(100% - 50px)"
language={script.script_type === 'notebook' ? 'json' : 'python'}
value={readOnlyContent ?? ''}
theme="vs"
options={{
readOnly: true,
domReadOnly: true,
minimap: { enabled: true },
lineNumbers: "on",
folding: true,
wordWrap: "on",
contextmenu: false,
automaticLayout: true,
}}
/>
)}
</div>
) : (
// 正常编辑模式:原有的 iframe + Monaco 预览逻辑
<div className={`editor-canvas ${sessionCache.size > 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 (
<section
key={scriptId}
className="embedded-jupyter"
style={{
position: isActive ? 'relative' : 'absolute',
visibility: isActive ? 'visible' : 'hidden',
pointerEvents: isActive ? 'auto' : 'none',
width: '100%',
height: '100%',
overflow: 'hidden'
}}
>
{editBusy
? <span className="button-spinner button-spinner--blue" />
: <Icon name="external" size={17} />}
{editBusy
? "正在准备编辑器…"
: isEditing
? "继续编辑"
: "打开编辑器"}
</button>
</div>
<div className="metadata-grid">
<div>
<span></span>
<strong>Python</strong>
</div>
<div>
<span></span>
<strong>
{script.visibility === "workspace"
? "Workspace"
: script.visibility === "public" ? "公开" : "私有"}
</strong>
</div>
<div>
<span></span>
<strong>{formatBytes(script.size_bytes)}</strong>
</div>
<div>
<span></span>
<strong>{formatTime(script.updated_at)}</strong>
</div>
</div>
<div className="preview-card">
<div className="preview-card__bar">
<div>
<span className="window-dot window-dot--red" />
<span className="window-dot window-dot--yellow" />
<span className="window-dot window-dot--green" />
<div className="embedded-jupyter__status">
<span>
<i />
Workspace Jupyter Server
</span>
<span>
{cached.session.session_status === "active" ? "Notebook Session · Python 3 Kernel" : "Jupyter 文本编辑"}
</span>
<code title={cached.session.runtime_id}>
Runtime {cached.session.runtime_id.slice(-8)}
</code>
</div>
<span>Python </span>
<em>{isEditing ? "Jupyter 中可编辑" : "平台只读预览"}</em>
</div>
<PythonPreview
workspaceId={script.workspace_id}
filePath={script.jupyter_path}
/>
</div>
<iframe
src={cached.jupyterUrl}
title={`${scriptId} Jupyter 编辑器`}
allow="clipboard-read; clipboard-write"
sandbox="allow-same-origin allow-scripts allow-forms allow-downloads allow-modals"
onLoad={(event) => confineJupyterFrame(event.currentTarget)}
/>
</section>
);
})}
<div className="integrity-row">
<span>
<Icon name="check" size={15} />
{/*{isEditing ? "Demo 无锁编辑会话已连接" : "工作副本已同步"}*/}
</span>
<span>SHA-256&nbsp; {shortHash(script.content_hash)}</span>
<span>
&nbsp;
{versionsLoading
? "加载中"
: latestVersion
? `${latestVersion.version_label} · ${latestVersion.versions_id}`
: "尚未发布"}
</span>
</div>
</section>
)) : null}
</div>
{/* 当前脚本没有缓存时的状态显示 */}
{!sessionCache.has(script.script_id) ? (
isNotebook ? (
<section
className={`editor-opening-state${openError ? " has-error" : ""}`}
aria-live="polite"
style={{ display: 'block' }}
>
<div className="editor-opening-state__icon">
{openError
? <Icon name="info" size={28} />
: <span className="button-spinner button-spinner--blue" />}
</div>
<strong>
{openError ? "Notebook 打开失败" : "正在打开 Notebook"}
</strong>
<p>
{openError
? openError
: "正在获取编辑锁并连接 Workspace Jupyter Server…"}
</p>
{openError && (
<button
className="open-editor-button"
type="button"
disabled={editBusy}
onClick={onOpenEditor}
>
{editBusy
? <span className="button-spinner button-spinner--blue" />
: <Icon name="refresh" size={16} />}
{editBusy ? "正在重试…" : "重试打开"}
</button>
)}
</section>
) : (
<section className="script-overview">
<div className="script-overview__header">
<div>
<span className="section-kicker">PYTHON SCRIPT</span>
<h2>{script.script_name}</h2>
<p>{script.relative_path}</p>
</div>
<button
className={`open-editor-button${isEditing ? " is-editing" : ""}`}
type="button"
disabled={editBusy}
onClick={onOpenEditor}
>
{editBusy
? <span className="button-spinner button-spinner--blue" />
: <Icon name="external" size={17} />}
{editBusy
? "正在准备编辑器…"
: isEditing
? "继续编辑"
: "打开编辑器"}
</button>
</div>
<div className="metadata-grid">
<div>
<span></span>
<strong>Python</strong>
</div>
<div>
<span></span>
<strong>
{script.visibility === "workspace"
? "Workspace"
: script.visibility === "public" ? "公开" : "私有"}
</strong>
</div>
<div>
<span></span>
<strong>{formatBytes(script.size_bytes)}</strong>
</div>
<div>
<span></span>
<strong>{formatTime(script.updated_at)}</strong>
</div>
</div>
<div className="preview-card">
<div className="preview-card__bar">
<div>
<span className="window-dot window-dot--red" />
<span className="window-dot window-dot--yellow" />
<span className="window-dot window-dot--green" />
</div>
<span>Python </span>
<em>{isEditing ? "Jupyter 中可编辑" : "平台只读预览"}</em>
</div>
<PythonPreview
workspaceId={script.workspace_id}
filePath={script.jupyter_path}
/>
</div>
<div className="integrity-row">
<span>
<Icon name="check" size={15} />
</span>
<span>SHA-256&nbsp; {shortHash(script.content_hash)}</span>
<span>
&nbsp;
{versionsLoading
? "加载中"
: latestVersion
? `${latestVersion.version_label} · ${latestVersion.versions_id}`
: "尚未发布"}
</span>
</div>
</section>
)
) : null}
</div>
)}
</>
);
}