145 lines
4.9 KiB
TypeScript
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>
|
|
);
|
|
}
|