fix:脚本文件标签栏

This commit is contained in:
xiaozhu
2026-08-04 15:53:17 +08:00
parent eace1109ac
commit a4019b1a74
3 changed files with 199 additions and 28 deletions
@@ -131,6 +131,7 @@ function AuthenticatedModelPlatformApp() {
const [scripts, setScripts] = useState<ScriptItem[]>([]);
const [directories, setDirectories] = useState<WorkspaceDirectory[]>([]);
const [selectedId, setSelectedId] = useState<string | null>(null);
const [openTabIds, setOpenTabIds] = useState<string[]>([]);
const [keyword, setKeyword] = useState("");
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
@@ -377,12 +378,52 @@ function AuthenticatedModelPlatformApp() {
const selected = scripts.find((item) => item.script_id === selectedId) ?? null;
const selectScript = (scriptId: string | null) => {
setSelectedId(scriptId);
};
const openTab = (scriptId: string) => {
if (selectedIdRef.current !== scriptId) {
editorOpenRequestRef.current += 1;
setEditorOpenError(null);
}
selectedIdRef.current = scriptId;
setSelectedId(scriptId);
setOpenTabIds((current) => {
if (current.includes(scriptId)) {
return current;
}
return [...current, scriptId];
});
};
const closeTab = async (scriptId: string, event?: ReactMouseEvent) => {
event?.stopPropagation();
// 如果关闭的是正在编辑的脚本,先释放编辑锁
if (editSessionRef.current?.script_id === scriptId) {
await endEditing(false, false);
}
setOpenTabIds((current) => {
const index = current.indexOf(scriptId);
if (index === -1) return current;
const newTabs = current.filter((id) => id !== scriptId);
if (selectedId === scriptId) {
const nextId = newTabs[index] ?? newTabs[index - 1] ?? null;
setSelectedId(nextId);
selectedIdRef.current = nextId;
}
return newTabs;
});
};
const switchTab = (scriptId: string) => {
if (selectedIdRef.current !== scriptId) {
editorOpenRequestRef.current += 1;
setEditorOpenError(null);
}
setSelectedId(scriptId);
selectedIdRef.current = scriptId;
};
const openScriptEditor = async (
@@ -503,12 +544,15 @@ function AuthenticatedModelPlatformApp() {
selected?.script_type,
]);
const endEditing = async (closeTab = false, showToast = true) => {
const endEditing = async (closeTabFlag = true, showToast = true) => {
editorOpenRequestRef.current += 1;
setEditorOpenError(null);
const active = editSessionRef.current;
const scriptId = active?.script_id;
if (!active) {
if (closeTab) selectScript(null);
if (closeTabFlag && scriptId) {
void closeTab(scriptId);
}
return;
}
setEditBusy(true);
@@ -517,7 +561,9 @@ function AuthenticatedModelPlatformApp() {
setEmbeddedJupyterUrl(null);
setEditSession(null);
editSessionRef.current = null;
if (closeTab) selectScript(null);
if (closeTabFlag && scriptId) {
void closeTab(scriptId);
}
if (showToast) {
setToast({
tone: "success",
@@ -613,7 +659,7 @@ function AuthenticatedModelPlatformApp() {
try {
const created = await api.createScript(form);
setScripts((items) => [created, ...items]);
selectScript(created.script_id);
openTab(created.script_id);
setCreateOpen(false);
setForm(initialForm);
setToast({
@@ -958,7 +1004,7 @@ function AuthenticatedModelPlatformApp() {
scripts={group.scripts}
directories={group.directories}
selectedId={selectedId}
onSelect={selectScript}
onSelect={openTab}
onContextMenu={
group.user?.user_id === user?.user_id
? showContextMenu
@@ -1014,17 +1060,19 @@ function AuthenticatedModelPlatformApp() {
}
latestVersion={latestVersion}
versionsLoading={latestVersionLoading}
openTabs={openTabIds.map((id) => {
const s = scripts.find((item) => item.script_id === id);
return {
scriptId: id,
scriptName: s?.script_name ?? "未知",
scriptType: s?.script_type ?? "notebook",
};
})}
onOpenEditor={() => void openScriptEditor(selected)}
onEndEditing={() => void endEditing()}
onClose={() => {
if (
editSessionRef.current?.script_id === selected.script_id
) {
void endEditing(true);
} else {
selectScript(null);
}
}}
onClose={(scriptId, event) => void closeTab(scriptId, event)}
onSwitchTab={switchTab}
onNewTab={() => openCreateDialog("")}
onPublish={() => openPublishDialog(selected)}
onInfo={setToast}
/>
@@ -3,8 +3,11 @@ import type {
ActiveEditSession,
LatestVersion,
ScriptItem,
ScriptType,
} from "../../services/api";
import { scriptIcon } from "./WorkspaceTree";
import type { MouseEvent as ReactMouseEvent } from "react";
import { useRef, useEffect } from "react";
type ToastState = {
tone: "success" | "error" | "info";
@@ -19,9 +22,12 @@ type ScriptWorkspaceProps = {
openError: string | null;
latestVersion: LatestVersion | null;
versionsLoading: boolean;
openTabs: Array<{ scriptId: string; scriptName: string; scriptType: ScriptType }>;
onOpenEditor: () => void;
onEndEditing: () => void;
onClose: () => void;
onClose: (scriptId: string, event?: ReactMouseEvent) => void;
onSwitchTab: (scriptId: string) => void;
onNewTab: () => void;
onPublish: () => void;
onInfo: (toast: ToastState) => void;
};
@@ -91,32 +97,88 @@ export function ScriptWorkspace({
openError,
latestVersion,
versionsLoading,
openTabs,
onOpenEditor,
onEndEditing,
onClose,
onSwitchTab,
onNewTab,
onPublish,
onInfo,
}: ScriptWorkspaceProps) {
const tabbarRef = useRef<HTMLDivElement | null>(null);
const isNotebook = script.script_type === "notebook";
const isEditing = editSession?.session_status === "active";
const scroll = (direction: "left" | "right") => {
const tabbar = tabbarRef.current;
if (!tabbar) return;
const scrollAmount = 200;
tabbar.scrollBy({
left: direction === "left" ? -scrollAmount : scrollAmount,
behavior: "smooth",
});
};
useEffect(() => {
const tabbar = tabbarRef.current;
if (!tabbar) return;
const activeTab = tabbar.querySelector(".editor-tab--active") as HTMLElement | null;
if (activeTab) {
const tabbarRect = tabbar.getBoundingClientRect();
const tabRect = activeTab.getBoundingClientRect();
if (tabRect.right > tabbarRect.right || tabRect.left < tabbarRect.left) {
activeTab.scrollIntoView({ behavior: "smooth", inline: "center" });
}
}
}, [script.script_id]);
return (
<>
<div className="tabbar">
<div className="editor-tab editor-tab--active">
<span className={`file-icon file-icon--${script.script_type}`}>
<Icon name={scriptIcon(script)} size={16} />
</span>
<span>{script.script_name}</span>
<button type="button" aria-label="关闭标签" onClick={onClose}>
<Icon name="close" size={14} />
<button
className="tabbar-scroll-btn tabbar-scroll-btn--left"
type="button"
aria-label="向左滚动"
onClick={() => scroll("left")}
>
<Icon name="chevron" size={16} />
</button>
<div className="tabbar-scroll-content" ref={tabbarRef}>
{openTabs.map((tab) => (
<div
key={tab.scriptId}
className={`editor-tab${tab.scriptId === script.script_id ? " editor-tab--active" : ""}`}
onClick={() => onSwitchTab(tab.scriptId)}
>
<span className={`file-icon file-icon--${tab.scriptType}`}>
<Icon name={tab.scriptType === "notebook" ? "notebook" : "python"} size={16} />
</span>
<span>{tab.scriptName}</span>
<button
type="button"
aria-label="关闭标签"
onClick={(event) => onClose(tab.scriptId, event)}
>
<Icon name="close" size={14} />
</button>
</div>
))}
<button
className="new-tab"
type="button"
onClick={onNewTab}
>
<Icon name="plus" size={17} />
</button>
</div>
<button
className="new-tab"
className="tabbar-scroll-btn tabbar-scroll-btn--right"
type="button"
onClick={() => onInfo({ tone: "info", message: "请从左侧选择或新建脚本" })}
aria-label="向右滚动"
onClick={() => scroll("right")}
>
<Icon name="plus" size={17} />
<Icon name="chevron" size={16} />
</button>
</div>