update: PythonEditor.tsx

This commit is contained in:
tao.chen
2026-08-12 16:58:00 +08:00
parent 2fcdf51cfc
commit a86432b102
5 changed files with 576 additions and 85 deletions
@@ -4,15 +4,16 @@ import type {
LatestVersion,
ScriptItem,
ScriptType,
} from "../../services/api";
} 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 } from "./state/scriptWorkspaceStore";
import { useAuth } from "../../context/AuthContext";
import { getScriptContent } from "../../services/api";
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";
@@ -28,6 +29,7 @@ interface CachedSession {
type ScriptWorkspaceProps = {
script: ScriptItem;
scripts: ScriptItem[];
sessionCache: Map<string, CachedSession>;
editSession: ActiveEditSession | null;
jupyterUrl: string | null;
@@ -43,6 +45,13 @@ type ScriptWorkspaceProps = {
onNewTab: () => void;
onPublish: () => void;
onInfo: (toast: ToastState) => void;
pythonEditorBuffers: Record<string, PythonEditorBuffer>;
onOpenPythonEditor: () => void;
onSetPythonEditorContent: (scriptId: string, v: string) => void;
onSavePythonEditor: (scriptId: string) => void;
onExitPythonEditor: (scriptId: string) => void;
onClosePythonTab: (scriptId: string) => void;
};
function formatTime(value: string) {
@@ -104,6 +113,7 @@ function confineJupyterFrame(frame: HTMLIFrameElement): void {
export function ScriptWorkspace({
script,
scripts,
sessionCache,
editSession,
jupyterUrl,
@@ -119,13 +129,30 @@ export function ScriptWorkspace({
onNewTab,
onPublish,
onInfo,
pythonEditorBuffers,
onOpenPythonEditor,
onSetPythonEditorContent,
onSavePythonEditor,
onExitPythonEditor,
onClosePythonTab,
}: ScriptWorkspaceProps) {
const { user } = useAuth();
const tabbarRef = useRef<HTMLDivElement | null>(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<string | null>(null);
@@ -249,7 +276,23 @@ export function ScriptWorkspace({
<strong>{script.script_name}</strong>
</div>
<div className="editor-toolbar__actions">
{isEditing ? (
{isPythonEditing ? (
<>
{showSaveButton && (
<button
type="button"
className={`editor-save-button${activePythonBuf?.saving ? " is-saving" : ""}`}
disabled={saveDisabled}
onClick={() => onSavePythonEditor(script.script_id)}
>
{activePythonBuf?.saving
? <><span className="button-spinner button-spinner--blue" /> </>
: "保存"}
</button>
)}
<button type="button" className="end-edit-button" onClick={() => onExitPythonEditor(script.script_id)}></button>
</>
) : isEditing ? (
<button
className="end-edit-button"
type="button"
@@ -327,6 +370,40 @@ export function ScriptWorkspace({
) : (
// 正常编辑模式:原有的 iframe + Monaco 预览逻辑
<div className={`editor-canvas ${sessionCache.size > 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 (
<section
key={tab.scriptId}
className="python-editor-mount"
style={{
position: isActive ? "relative" : "absolute",
visibility: isActive ? "visible" : "hidden",
pointerEvents: isActive ? "auto" : "none",
width: "100%",
height: "100%",
}}
>
<PythonEditor
scriptId={tab.scriptId}
script={tabScript}
initialContent={buf.initialContent ?? ""}
loading={buf.initialContent === null && !buf.loadError}
loadError={buf.loadError}
saving={buf.saving}
dirty={buf.dirty}
onChange={onSetPythonEditorContent}
onSave={onSavePythonEditor}
onEndEditing={onExitPythonEditor}
onCloseTab={onClosePythonTab}
/>
</section>
);
})}
{/* 渲染所有缓存的 iframe */}
{Array.from(sessionCache.entries()).map(([scriptId, cached]) => {
const isActive = scriptId === script.script_id;
@@ -366,67 +443,67 @@ export function ScriptWorkspace({
);
})}
{/* 当前脚本没有缓存时的状态显示 */}
{!sessionCache.has(script.script_id) ? (
isNotebook ? (
<section
className={`editor-opening-state${openError ? " has-error" : ""}`}
aria-live="polite"
style={{ display: 'block' }}
{/* 当前脚本没有缓存时的状态显示 */}
{!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>
) : isPython && !activePythonBuf ? (
<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}
>
<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>
{editBusy
? <span className="button-spinner button-spinner--blue" />
: <Icon name="external" size={17} />}
{editBusy
? "正在准备编辑器…"
: isEditing
? "继续编辑"
: "打开编辑器"}
</button>
</div>
<div className="metadata-grid">
<div className="metadata-grid">
<div>
<span></span>
<strong>Python</strong>
@@ -448,8 +525,7 @@ export function ScriptWorkspace({
<strong>{formatTime(script.updated_at)}</strong>
</div>
</div>
<div className="preview-card">
<div className="preview-card">
<div className="preview-card__bar">
<div>
<span className="window-dot window-dot--red" />
@@ -465,23 +541,23 @@ export function ScriptWorkspace({
/>
</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 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):null}
</div>
)}
</>