update: PythonEditor.tsx
This commit is contained in:
@@ -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 {shortHash(script.content_hash)}</span>
|
||||
<span>
|
||||
稳定版本
|
||||
{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 {shortHash(script.content_hash)}</span>
|
||||
<span>
|
||||
稳定版本
|
||||
{versionsLoading
|
||||
? "加载中"
|
||||
: latestVersion
|
||||
? `${latestVersion.version_label} · ${latestVersion.versions_id}`
|
||||
: "尚未发布"}
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
) : null):null}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user