update: PythonEditor.tsx
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user