130 lines
3.7 KiB
TypeScript
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>
|
|
);
|
|
}
|