update:原生确认框替换为自定义弹窗

This commit is contained in:
xiaozhu
2026-09-01 17:23:31 +08:00
parent 149e85cf10
commit 026cd085c6
15 changed files with 452 additions and 83 deletions
+39 -26
View File
@@ -13,9 +13,10 @@ import {
import { WelcomePanel } from "./WelcomePanel";
import { PublishModal } from "./PublishModal";
import { ScriptExplorer } from "./ScriptExplorer";
import { ScriptsPendingConfirmDialog } from "./ScriptsPendingConfirm";
import { TreeContextMenu } from "./TreeContextMenu";
import { VersionReceiptModal } from "./VersionReceiptModal";
import { useScriptsPendingConfirm } from "./useScriptsPendingConfirm";
import { getSessionCache, useScriptWorkspaceStore } from "./state/scriptWorkspaceStore";
import { useUiStore } from "./state/uiStore";
import { toast } from "sonner";
@@ -75,6 +76,18 @@ export default function ScriptsPage() {
const submitPublish = useScriptWorkspaceStore((s) => s.submitPublish);
const refreshReadOnlyContent = useScriptWorkspaceStore((s) => s.refreshReadOnlyContent);
const {
pendingConfirm,
setPendingConfirm,
requestCloseTab,
handleConfirm,
} = useScriptsPendingConfirm(scripts, {
deleteScript,
deleteDataResource,
deleteDirectory,
closeTab,
});
// ui store
const pushToast = (notice: { tone: "success" | "error" | "info"; message: string }) => {
if (notice.tone === "error") toast.error(notice.message);
@@ -206,27 +219,11 @@ export default function ScriptsPage() {
};
}, [contextMenu, closeContextMenu]);
// python editor handlers with scriptId forwarding
const handleSetPythonEditorContent = (scriptId: string, value: string) => {
setPythonEditorContent(scriptId, value);
};
const handleSavePythonEditor = (scriptId: string) => {
void savePythonEditor(scriptId);
};
const handleExitPythonEditor = (scriptId: string) => {
exitPythonEditor(scriptId);
};
const handleClosePythonTab = (scriptId: string) => {
void closeTab(scriptId);
};
// 5) handlers
// 5) handlers
const SCRIPT_EXTS = [".py", ".ipynb"];
const DATA_EXTS = [".csv", ".xlsx", ".xls", ".tsv", ".json", ".parquet", ".txt"];
const matchesExt = (name: string, exts: string[]) =>
exts.some((ext) => name.toLowerCase().endsWith(ext));
const handleUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
const files = Array.from(event.target.files ?? []);
event.target.value = "";
@@ -381,17 +378,17 @@ export default function ScriptsPage() {
void endEditing();
}
}}
onClose={(scriptId, event) => void closeTab(scriptId, event)}
onClose={(scriptId, event) => void requestCloseTab(scriptId, event)}
onSwitchTab={switchTab}
onNewTab={() => openCreateDialog("")}
onPublish={() => openPublishDialog(selected)}
scripts={scripts}
pythonEditorBuffers={pythonEditorBuffers}
onOpenPythonEditor={() => void openPythonEditor(selected)}
onSetPythonEditorContent={handleSetPythonEditorContent}
onSavePythonEditor={handleSavePythonEditor}
onExitPythonEditor={handleExitPythonEditor}
onClosePythonTab={handleClosePythonTab}
onSetPythonEditorContent={setPythonEditorContent}
onSavePythonEditor={(scriptId) => void savePythonEditor(scriptId)}
onExitPythonEditor={exitPythonEditor}
onClosePythonTab={(scriptId) => void requestCloseTab(scriptId)}
onInfo={(t) => pushToast(t)}
/>
) : (
@@ -429,19 +426,35 @@ export default function ScriptsPage() {
selectScript(scriptId);
closeContextMenu();
}}
onRemoveScript={(s) => void deleteScript(s)}
onRemoveScript={(s) => setPendingConfirm({ kind: "script", script: s })}
onToggleLock={(s) => void toggleScriptLock(s)}
onOpenCreateDialog={(parentPath, scriptType) =>
openCreateDialog(parentPath, scriptType)}
onOpenFolderDialog={(parentPath) => openFolderDialog(parentPath)}
onChooseUpload={(parentPath) => triggerUpload(parentPath ?? "")}
onRemoveDirectory={(p) => void deleteDirectory(p)}
onRemoveDirectory={(p) =>
setPendingConfirm({ kind: "directory", path: p })}
onCopyResourcePath={(p) => void handleCopyResourcePath(p)}
onPreviewResource={openResourcePreview}
onRemoveResource={(id) => void deleteDataResource(id)}
onRemoveResource={(id) => {
const resource = dataResources.find((item) => item.resource_id === id);
setPendingConfirm({
kind: "resource",
id,
name: resource?.resource_name ?? id,
});
}}
onClose={closeContextMenu}
/>
<ScriptsPendingConfirmDialog
pending={pendingConfirm}
onOpenChange={(open) => {
if (!open) setPendingConfirm(null);
}}
onConfirm={() => void handleConfirm()}
/>
<DataResourcePreviewDialog
target={resourcePreview}
onClose={() => setResourcePreview(null)}
@@ -0,0 +1,66 @@
import { ConfirmDialog } from "~/components/common/ConfirmDialog";
import type { ScriptItem } from "~/services/api";
export type ScriptsPendingConfirm =
| { kind: "script"; script: ScriptItem }
| { kind: "resource"; id: string; name: string }
| { kind: "directory"; path: string }
| { kind: "close-tab"; id: string; name: string };
type ScriptsPendingConfirmDialogProps = {
pending: ScriptsPendingConfirm | null;
onOpenChange: (open: boolean) => void;
onConfirm: () => void | Promise<void>;
};
function copyFor(pending: ScriptsPendingConfirm) {
switch (pending.kind) {
case "script":
return {
title: "确定删除文件?",
description: `确定删除文件"${pending.script.script_name}"吗?稳定版本会保留。`,
confirmLabel: "删除",
destructive: true,
};
case "resource":
return {
title: "确定删除数据资源?",
description: `确定删除数据资源"${pending.name}"吗?稳定版本会保留。`,
confirmLabel: "删除",
destructive: true,
};
case "directory":
return {
title: "确定删除文件夹?",
description: `确定递归删除文件夹"${pending.path}"及其内容吗?稳定版本会保留。`,
confirmLabel: "删除",
destructive: true,
};
case "close-tab":
return {
title: "确定关闭标签?",
description: `当前脚本有未保存修改,确定关闭 "${pending.name}" 吗?`,
confirmLabel: "关闭",
destructive: false,
};
}
}
export function ScriptsPendingConfirmDialog({
pending,
onOpenChange,
onConfirm,
}: ScriptsPendingConfirmDialogProps) {
const copy = pending ? copyFor(pending) : null;
return (
<ConfirmDialog
open={pending !== null}
onOpenChange={onOpenChange}
title={copy?.title ?? ""}
description={copy?.description ?? ""}
confirmLabel={copy?.confirmLabel}
destructive={copy?.destructive}
onConfirm={onConfirm}
/>
);
}
@@ -151,7 +151,10 @@ export function TreeContextMenu({
className={dangerItemClass}
type="button"
role="menuitem"
onClick={() => onRemoveScript(contextMenu.script!)}
onClick={() => {
onRemoveScript(contextMenu.script!);
onClose();
}}
>
<X size={16} />
@@ -204,7 +207,10 @@ export function TreeContextMenu({
className={dangerItemClass}
type="button"
role="menuitem"
onClick={() => onRemoveDirectory(contextMenu.path)}
onClick={() => {
onRemoveDirectory(contextMenu.path);
onClose();
}}
>
<X size={16} />
@@ -164,7 +164,7 @@ export const createEditSessionSlice: StateCreator<
const scriptId = active?.script_id;
if (!active) {
if (closeTabFlag && scriptId) {
await get().closeTab(scriptId);
await get().closeTab(scriptId, undefined, { discardDirty: true });
}
return;
}
@@ -174,7 +174,7 @@ export const createEditSessionSlice: StateCreator<
applyEditSessionState((p) => set(p), null, null);
if (scriptId) sessionCache.delete(scriptId);
if (closeTabFlag && scriptId) {
await get().closeTab(scriptId);
await get().closeTab(scriptId, undefined, { discardDirty: true });
}
if (showToast) {
pushToast("success", `${active.script_name} 的编辑锁已释放`);
@@ -215,9 +215,6 @@ export const createMutationsSlice: StateCreator<
deleteDataResource: async (resourceId) => {
const api = requireApi();
useUiStore.getState().closeContextMenu();
if (!window.confirm(`确定删除数据资源吗?稳定版本会保留。`)) {
return;
}
try {
await api.deleteResource(resourceId);
set((state) => ({
@@ -287,11 +284,6 @@ export const createMutationsSlice: StateCreator<
deleteScript: async (script: ScriptItem) => {
const api = requireApi();
useUiStore.getState().closeContextMenu();
if (
!window.confirm(`确定删除文件"${script.script_name}"吗?稳定版本会保留。`)
) {
return;
}
const editSession = getEditSession();
if (editSession?.script_id === script.script_id) {
await get().endEditing(false, false);
@@ -323,11 +315,6 @@ export const createMutationsSlice: StateCreator<
deleteDirectory: async (path) => {
const api = requireApi();
useUiStore.getState().closeContextMenu();
if (
!window.confirm(`确定递归删除文件夹"${path}"及其内容吗?稳定版本会保留。`)
) {
return;
}
const activeScript = get().scripts.find(
(item) => item.script_id === getEditSession()?.script_id,
);
@@ -62,14 +62,11 @@ export const createSelectionSlice: StateCreator<
}));
},
closeTab: async (id, event) => {
closeTab: async (id, event, options) => {
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?.dirty && !buffer.saving && !options?.discardDirty) {
return false;
}
if (buffer) {
get().exitPythonEditor(id);
@@ -91,6 +88,7 @@ export const createSelectionSlice: StateCreator<
}
return { openTabIds: newTabs, selectedId: nextSelected };
});
return true;
},
switchTab: (id) => {
@@ -92,7 +92,8 @@ export type SelectionSliceActions = {
closeTab: (
id: string,
event?: { stopPropagation: () => void },
) => Promise<void>;
options?: { discardDirty?: boolean },
) => Promise<boolean>;
switchTab: (id: string) => void;
openPublishDialog: (script: ScriptItem) => void;
};
@@ -0,0 +1,54 @@
import { useState } from "react";
import type { ScriptItem } from "~/services/api";
import type { ScriptsPendingConfirm } from "./ScriptsPendingConfirm";
type DeleteActions = {
deleteScript: (script: ScriptItem) => Promise<void>;
deleteDataResource: (resourceId: string) => Promise<void>;
deleteDirectory: (path: string) => Promise<void>;
closeTab: (
id: string,
event?: { stopPropagation: () => void },
options?: { discardDirty?: boolean },
) => Promise<boolean>;
};
export function useScriptsPendingConfirm(
scripts: ScriptItem[],
actions: DeleteActions,
) {
const [pendingConfirm, setPendingConfirm] =
useState<ScriptsPendingConfirm | null>(null);
const requestCloseTab = async (
scriptId: string,
event?: { stopPropagation: () => void },
) => {
const closed = await actions.closeTab(scriptId, event);
if (closed) return;
const name =
scripts.find((s) => s.script_id === scriptId)?.script_name ?? "该脚本";
setPendingConfirm({ kind: "close-tab", id: scriptId, name });
};
const handleConfirm = async () => {
if (!pendingConfirm) return;
const target = pendingConfirm;
setPendingConfirm(null);
if (target.kind === "script") await actions.deleteScript(target.script);
else if (target.kind === "resource") {
await actions.deleteDataResource(target.id);
} else if (target.kind === "directory") {
await actions.deleteDirectory(target.path);
} else {
await actions.closeTab(target.id, undefined, { discardDirty: true });
}
};
return {
pendingConfirm,
setPendingConfirm,
requestCloseTab,
handleConfirm,
};
}