Files
model-platform/frontend/app/features/platform/PythonEditor.tsx
T
2026-08-12 16:58:00 +08:00

130 lines
3.7 KiB
TypeScript

import { useEffect, useRef, useState } from "react";
import Editor from "@monaco-editor/react";
import Icon from "../../components/common/Icon";
import type { ScriptItem } from "../../services/api";
interface PythonEditorProps {
script: ScriptItem;
scriptId: string;
initialContent: string;
saving: boolean;
dirty: boolean;
loading: boolean;
loadError?: string | null;
onChange: (scriptId: string, value: string) => void;
onSave: (scriptId: string) => void;
onEndEditing: (scriptId: string) => void;
onCloseTab: (scriptId: string) => void;
}
export function PythonEditor({
script,
scriptId,
initialContent,
saving,
dirty,
loading,
loadError,
onChange,
onSave,
onEndEditing,
onCloseTab,
}: PythonEditorProps) {
const containerRef = useRef<HTMLDivElement | null>(null);
const [value, setValue] = useState(initialContent);
useEffect(() => {
const onKey = (e: KeyboardEvent) => {
const mod = e.metaKey || e.ctrlKey;
if (!mod) return;
if (e.key.toLowerCase() === "s") {
e.preventDefault();
if (dirty && !saving) onSave(scriptId);
} else if (e.key.toLowerCase() === "w") {
e.preventDefault();
onCloseTab(scriptId);
}
};
containerRef.current?.addEventListener("keydown", onKey);
return () => containerRef.current?.removeEventListener("keydown", onKey);
}, [dirty, saving, onSave, onCloseTab, scriptId]);
useEffect(() => {
if (!dirty || saving) return;
const t = window.setTimeout(() => onSave(scriptId), 30_000);
return () => window.clearTimeout(t);
}, [value, dirty, saving, onSave, scriptId]);
const hasSyncedRef = useRef(false);
useEffect(() => {
if (!loading && initialContent !== null && !hasSyncedRef.current) {
setValue(initialContent);
hasSyncedRef.current = true;
}
}, [loading, initialContent]);
if (loading) {
return (
<div className="python-editor" ref={containerRef}>
<section className="editor-opening-state" aria-live="polite" style={{ display: "block" }}>
<div className="editor-opening-state__icon">
<span className="button-spinner button-spinner--blue" />
</div>
<strong>正在打开 Python 编辑器</strong>
<p>正在加载文件内容…</p>
</section>
</div>
);
}
if (loadError) {
return (
<div className="python-editor" ref={containerRef}>
<section className="editor-opening-state has-error" aria-live="polite" style={{ display: "block" }}>
<div className="editor-opening-state__icon">
<Icon name="info" size={28} />
</div>
<strong>Python 编辑器打开失败</strong>
<p>{loadError}</p>
</section>
</div>
);
}
return (
<div className="python-editor" ref={containerRef}>
{dirty && <div className="editor-dirty-banner">未保存修改</div>}
<div className="python-editor__status">
<span>
<i /> Workspace Python Editor
</span>
<span>{script.relative_path}</span>
</div>
<div className="python-editor__canvas">
<Editor
height="100%"
language="python"
theme="vs"
value={value}
onChange={(v) => {
setValue(v ?? "");
onChange(scriptId, v ?? "");
}}
options={{
minimap: { enabled: initialContent.length > 5000 },
wordWrap: "on",
fontSize: 13,
automaticLayout: true,
renderLineHighlight: "gutter",
contextmenu: false,
dragAndDrop: false,
scrollBeyondLastLine: false,
}}
/>
</div>
</div>
);
}