fix:脚本文件标签栏
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user