Files
model-platform/frontend/app/features/platform/PythonEditor.tsx
T
2026-09-02 10:10:41 +08:00

145 lines
4.9 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="flex h-full min-h-0 flex-col bg-white" ref={containerRef}>
<section
className="mx-auto my-16 flex w-[min(520px,calc(100%-48px))] min-h-[260px] flex-col items-center justify-center rounded-[10px] border border-[#dce5ed] bg-white/95 p-9 text-center text-[#66788d] shadow-[0_12px_34px_rgb(31_64_98/7%)]"
aria-live="polite"
>
<div className="mb-4 grid size-[54px] place-items-center rounded-full bg-[#edf6ff] text-[#247bc5]">
<span className="button-spinner button-spinner--blue size-6 border-[3px]" />
</div>
<strong className="text-base text-[#1d344d]">正在打开 Python 编辑器</strong>
<p className="mt-[9px] max-w-[430px] text-xs leading-[1.7] text-[#7a8a9b]">
正在加载文件内容…
</p>
</section>
</div>
);
}
if (loadError) {
return (
<div className="flex h-full min-h-0 flex-col bg-white" ref={containerRef}>
<section
className="mx-auto my-16 flex w-[min(520px,calc(100%-48px))] min-h-[260px] flex-col items-center justify-center rounded-[10px] border border-[#f0cfcc] bg-white/95 p-9 text-center text-[#66788d] shadow-[0_12px_34px_rgb(31_64_98/7%)]"
aria-live="polite"
>
<div className="mb-4 grid size-[54px] place-items-center rounded-full bg-[#fff1f0] text-[#c84f49]">
<Icon name="info" size={28} />
</div>
<strong className="text-base text-[#1d344d]">Python 编辑器打开失败</strong>
<p className="mt-[9px] max-w-[430px] text-xs leading-[1.7] text-[#7a8a9b] [overflow-wrap:anywhere]">
{loadError}
</p>
</section>
</div>
);
}
return (
<div className="flex h-full min-h-0 flex-col bg-white" ref={containerRef}>
{dirty && (
<div className="flex items-center gap-2 border-b border-[#f0d8a8] bg-[#fff7e6] px-3.5 py-1.5 text-xs font-medium text-[#9b6a18] before:text-[10px] before:text-[#d49b00] before:content-['●']">
未保存修改
</div>
)}
<div className="flex items-center gap-4 border-b border-[#e6e8ec] bg-[#f5f7fa] px-3.5 py-1 text-[11px] text-[#6b7280]">
<span className="inline-flex items-center gap-1.5">
<i className="size-1.5 rounded-full bg-[#20b77d]" />
Workspace Python Editor
</span>
<span>{script.relative_path}</span>
</div>
<div className="relative min-h-0 flex-1 overflow-hidden">
<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>
);
}