update: PythonEditor.tsx

This commit is contained in:
tao.chen
2026-08-12 16:58:00 +08:00
parent 2fcdf51cfc
commit a86432b102
5 changed files with 576 additions and 85 deletions
@@ -20,6 +20,15 @@ type CachedSession = {
lastActiveTime: number;
};
export type PythonEditorBuffer = {
initialContent: string | null;
content: string | null;
dirty: boolean;
saving: boolean;
initial: boolean;
loadError: string | null;
};
// 模块级可变 holder(非响应式,避免 React 重渲)
const sessionCache = new Map<string, CachedSession>();
let _selectedId: string | null = null;
@@ -29,6 +38,7 @@ let _editorOpenRequest = 0;
let _api: WorkspaceBoundApi | null = null;
let _previewController: AbortController | null = null;
let _previewRequest = 0;
let _pythonEditorOpeningIds = new Set<string>();
export const bindScriptWorkspaceApi = (api: WorkspaceBoundApi | null) => {
_api = api;
@@ -68,6 +78,8 @@ type State = {
previewLoading: boolean;
previewError: string | null;
pythonEditorBuffers: Record<string, PythonEditorBuffer>;
// actions
setApiOnline: (online: boolean) => void;
setKeyword: (keyword: string) => void;
@@ -79,6 +91,11 @@ type State = {
switchTab: (id: string) => void;
openScriptEditor: (script: ScriptItem, showToast?: boolean) => Promise<void>;
endEditing: (closeTabFlag?: boolean, showToast?: boolean) => Promise<void>;
openPythonEditor: (script: ScriptItem, showToast?: boolean) => Promise<void>;
setPythonEditorContent: (scriptId: string, value: string) => void;
savePythonEditor: (scriptId: string) => Promise<void>;
exitPythonEditor: (scriptId: string) => void;
exitAllPythonEditors: () => void;
loadLatestVersion: (scriptId: string) => Promise<void>;
loadPreview: (workspaceId: string, filePath: string) => Promise<void>;
createScript: (form: NewScriptForm) => Promise<ScriptItem | null>;
@@ -148,6 +165,8 @@ export const useScriptWorkspaceStore = create<State>((set, get) => {
previewLoading: false,
previewError: null,
pythonEditorBuffers: {},
setApiOnline: (online) => set({ apiOnline: online }),
setKeyword: (keyword) => set({ keyword }),
@@ -161,6 +180,7 @@ export const useScriptWorkspaceStore = create<State>((set, get) => {
_editSession = null;
editSessionHandle.current = null;
sessionCache.clear();
_pythonEditorOpeningIds.clear();
set({
scripts: [],
directories: [],
@@ -176,6 +196,7 @@ export const useScriptWorkspaceStore = create<State>((set, get) => {
previewCodeSize: null,
previewLoading: false,
previewError: null,
pythonEditorBuffers: {},
});
},
@@ -236,6 +257,15 @@ export const useScriptWorkspaceStore = create<State>((set, get) => {
closeTab: async (id, event) => {
event?.stopPropagation();
const buffer = get().pythonEditorBuffers[id];
if (buffer?.dirty && !buffer.saving) {
const name = get().scripts.find((s) => s.script_id === id)?.script_name ?? "该脚本";
const ok = window.confirm(`当前脚本有未保存修改,确定关闭 "${name}" 吗?`);
if (!ok) return;
}
if (buffer) {
get().exitPythonEditor(id);
}
if (_editSession?.script_id === id) {
await get().endEditing(false, false);
}
@@ -255,6 +285,13 @@ export const useScriptWorkspaceStore = create<State>((set, get) => {
},
switchTab: (id) => {
const current = get().selectedId;
if (current && current !== id) {
const curBuffer = get().pythonEditorBuffers[current];
if (curBuffer?.dirty && !curBuffer.saving) {
void get().savePythonEditor(current);
}
}
if (_selectedId !== id) {
_editorOpenRequest += 1;
set({ editorOpenError: null });
@@ -268,6 +305,140 @@ export const useScriptWorkspaceStore = create<State>((set, get) => {
}
},
openPythonEditor: async (script, showToast = true) => {
if (!script) return;
if (_pythonEditorOpeningIds.has(script.script_id)) return;
_pythonEditorOpeningIds.add(script.script_id);
try {
const workspaceId = script.workspace_id;
const url =
`/jupyter/${workspaceId}/api/contents/${script.jupyter_path}` +
`?type=file&content=1&hash=1&format=text`;
const response = await fetch(url, {
credentials: "include",
cache: "no-store",
});
if (!response.ok) {
throw new Error(`加载文件失败: ${response.status}`);
}
const data = await response.json();
const content = typeof data.content === "string" ? data.content : "";
set((state) => ({
pythonEditorBuffers: {
...state.pythonEditorBuffers,
[script.script_id]: {
initialContent: content,
content,
dirty: false,
saving: false,
initial: true,
loadError: null,
},
},
}));
} catch (error) {
const message = error instanceof Error ? error.message : "打开 Python 编辑器失败";
set((state) => ({
pythonEditorBuffers: {
...state.pythonEditorBuffers,
[script.script_id]: {
initialContent: null,
content: null,
dirty: false,
saving: false,
initial: false,
loadError: message,
},
},
}));
pushToast("error", message);
} finally {
_pythonEditorOpeningIds.delete(script.script_id);
}
},
setPythonEditorContent: (scriptId, value) => {
set((state) => {
const buffer = state.pythonEditorBuffers[scriptId];
if (!buffer) return state;
return {
pythonEditorBuffers: {
...state.pythonEditorBuffers,
[scriptId]: {
...buffer,
content: value,
dirty: value !== buffer.initialContent,
},
},
};
});
},
savePythonEditor: async (scriptId) => {
const buffer = get().pythonEditorBuffers[scriptId];
if (!buffer || buffer.content === null || !buffer.dirty || buffer.saving) {
return;
}
const script = get().scripts.find((s) => s.script_id === scriptId);
if (!script) return;
const savedContent = buffer.content;
set((state) => ({
pythonEditorBuffers: {
...state.pythonEditorBuffers,
[scriptId]: { ...buffer, saving: true },
},
}));
try {
const api = requireApi();
await api.updateScript(scriptId, { content: savedContent });
set((state) => {
const current = state.pythonEditorBuffers[scriptId];
if (!current) return state;
return {
pythonEditorBuffers: {
...state.pythonEditorBuffers,
[scriptId]: {
...current,
initialContent: savedContent,
dirty: current.content !== savedContent,
saving: false,
initial: false,
},
},
};
});
pushToast("success", "已保存");
} catch (error) {
set((state) => {
const current = state.pythonEditorBuffers[scriptId];
if (!current) return state;
return {
pythonEditorBuffers: {
...state.pythonEditorBuffers,
[scriptId]: { ...current, saving: false },
},
};
});
pushToast(
"error",
error instanceof Error ? error.message : "保存失败",
);
}
},
exitPythonEditor: (scriptId) => {
set((state) => {
const next = { ...state.pythonEditorBuffers };
delete next[scriptId];
return { pythonEditorBuffers: next };
});
},
exitAllPythonEditors: () => {
_pythonEditorOpeningIds.clear();
set({ pythonEditorBuffers: {} });
},
openScriptEditor: async (script, showToast = true) => {
if (!script) return;
if (_editorOpening) return;