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 [scripts, setScripts] = useState<ScriptItem[]>([]);
const [directories, setDirectories] = useState<WorkspaceDirectory[]>([]); const [directories, setDirectories] = useState<WorkspaceDirectory[]>([]);
const [selectedId, setSelectedId] = useState<string | null>(null); const [selectedId, setSelectedId] = useState<string | null>(null);
const [openTabIds, setOpenTabIds] = useState<string[]>([]);
const [keyword, setKeyword] = useState(""); const [keyword, setKeyword] = useState("");
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false); const [refreshing, setRefreshing] = useState(false);
@@ -377,12 +378,52 @@ function AuthenticatedModelPlatformApp() {
const selected = scripts.find((item) => item.script_id === selectedId) ?? null; const selected = scripts.find((item) => item.script_id === selectedId) ?? null;
const selectScript = (scriptId: string | null) => { const selectScript = (scriptId: string | null) => {
setSelectedId(scriptId);
};
const openTab = (scriptId: string) => {
if (selectedIdRef.current !== scriptId) { if (selectedIdRef.current !== scriptId) {
editorOpenRequestRef.current += 1; editorOpenRequestRef.current += 1;
setEditorOpenError(null); setEditorOpenError(null);
} }
selectedIdRef.current = scriptId; selectedIdRef.current = scriptId;
setSelectedId(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 ( const openScriptEditor = async (
@@ -503,12 +544,15 @@ function AuthenticatedModelPlatformApp() {
selected?.script_type, selected?.script_type,
]); ]);
const endEditing = async (closeTab = false, showToast = true) => { const endEditing = async (closeTabFlag = true, showToast = true) => {
editorOpenRequestRef.current += 1; editorOpenRequestRef.current += 1;
setEditorOpenError(null); setEditorOpenError(null);
const active = editSessionRef.current; const active = editSessionRef.current;
const scriptId = active?.script_id;
if (!active) { if (!active) {
if (closeTab) selectScript(null); if (closeTabFlag && scriptId) {
void closeTab(scriptId);
}
return; return;
} }
setEditBusy(true); setEditBusy(true);
@@ -517,7 +561,9 @@ function AuthenticatedModelPlatformApp() {
setEmbeddedJupyterUrl(null); setEmbeddedJupyterUrl(null);
setEditSession(null); setEditSession(null);
editSessionRef.current = null; editSessionRef.current = null;
if (closeTab) selectScript(null); if (closeTabFlag && scriptId) {
void closeTab(scriptId);
}
if (showToast) { if (showToast) {
setToast({ setToast({
tone: "success", tone: "success",
@@ -613,7 +659,7 @@ function AuthenticatedModelPlatformApp() {
try { try {
const created = await api.createScript(form); const created = await api.createScript(form);
setScripts((items) => [created, ...items]); setScripts((items) => [created, ...items]);
selectScript(created.script_id); openTab(created.script_id);
setCreateOpen(false); setCreateOpen(false);
setForm(initialForm); setForm(initialForm);
setToast({ setToast({
@@ -958,7 +1004,7 @@ function AuthenticatedModelPlatformApp() {
scripts={group.scripts} scripts={group.scripts}
directories={group.directories} directories={group.directories}
selectedId={selectedId} selectedId={selectedId}
onSelect={selectScript} onSelect={openTab}
onContextMenu={ onContextMenu={
group.user?.user_id === user?.user_id group.user?.user_id === user?.user_id
? showContextMenu ? showContextMenu
@@ -1014,17 +1060,19 @@ function AuthenticatedModelPlatformApp() {
} }
latestVersion={latestVersion} latestVersion={latestVersion}
versionsLoading={latestVersionLoading} 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)} onOpenEditor={() => void openScriptEditor(selected)}
onEndEditing={() => void endEditing()} onEndEditing={() => void endEditing()}
onClose={() => { onClose={(scriptId, event) => void closeTab(scriptId, event)}
if ( onSwitchTab={switchTab}
editSessionRef.current?.script_id === selected.script_id onNewTab={() => openCreateDialog("")}
) {
void endEditing(true);
} else {
selectScript(null);
}
}}
onPublish={() => openPublishDialog(selected)} onPublish={() => openPublishDialog(selected)}
onInfo={setToast} onInfo={setToast}
/> />
@@ -3,8 +3,11 @@ import type {
ActiveEditSession, ActiveEditSession,
LatestVersion, LatestVersion,
ScriptItem, ScriptItem,
ScriptType,
} from "../../services/api"; } from "../../services/api";
import { scriptIcon } from "./WorkspaceTree"; import { scriptIcon } from "./WorkspaceTree";
import type { MouseEvent as ReactMouseEvent } from "react";
import { useRef, useEffect } from "react";
type ToastState = { type ToastState = {
tone: "success" | "error" | "info"; tone: "success" | "error" | "info";
@@ -19,9 +22,12 @@ type ScriptWorkspaceProps = {
openError: string | null; openError: string | null;
latestVersion: LatestVersion | null; latestVersion: LatestVersion | null;
versionsLoading: boolean; versionsLoading: boolean;
openTabs: Array<{ scriptId: string; scriptName: string; scriptType: ScriptType }>;
onOpenEditor: () => void; onOpenEditor: () => void;
onEndEditing: () => void; onEndEditing: () => void;
onClose: () => void; onClose: (scriptId: string, event?: ReactMouseEvent) => void;
onSwitchTab: (scriptId: string) => void;
onNewTab: () => void;
onPublish: () => void; onPublish: () => void;
onInfo: (toast: ToastState) => void; onInfo: (toast: ToastState) => void;
}; };
@@ -91,32 +97,88 @@ export function ScriptWorkspace({
openError, openError,
latestVersion, latestVersion,
versionsLoading, versionsLoading,
openTabs,
onOpenEditor, onOpenEditor,
onEndEditing, onEndEditing,
onClose, onClose,
onSwitchTab,
onNewTab,
onPublish, onPublish,
onInfo, onInfo,
}: ScriptWorkspaceProps) { }: ScriptWorkspaceProps) {
const tabbarRef = useRef<HTMLDivElement | null>(null);
const isNotebook = script.script_type === "notebook"; const isNotebook = script.script_type === "notebook";
const isEditing = editSession?.session_status === "active"; 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 ( return (
<> <>
<div className="tabbar"> <div className="tabbar">
<div className="editor-tab editor-tab--active"> <button
<span className={`file-icon file-icon--${script.script_type}`}> className="tabbar-scroll-btn tabbar-scroll-btn--left"
<Icon name={scriptIcon(script)} size={16} /> type="button"
</span> aria-label="向左滚动"
<span>{script.script_name}</span> onClick={() => scroll("left")}
<button type="button" aria-label="关闭标签" onClick={onClose}> >
<Icon name="close" size={14} /> <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> </button>
</div> </div>
<button <button
className="new-tab" className="tabbar-scroll-btn tabbar-scroll-btn--right"
type="button" type="button"
onClick={() => onInfo({ tone: "info", message: "请从左侧选择或新建脚本" })} aria-label="向右滚动"
onClick={() => scroll("right")}
> >
<Icon name="plus" size={17} /> <Icon name="chevron" size={16} />
</button> </button>
</div> </div>
+64 -3
View File
@@ -822,20 +822,81 @@ button {
align-items: stretch; align-items: stretch;
border-bottom: 1px solid #e5ebf1; border-bottom: 1px solid #e5ebf1;
background: #f7f9fb; background: #f7f9fb;
overflow: hidden;
position: relative;
}
.tabbar-scroll-content {
display: flex;
flex: 1;
overflow-x: auto;
overflow-y: hidden;
scroll-behavior: smooth;
}
.tabbar-scroll-content::-webkit-scrollbar {
height: 6px;
}
.tabbar-scroll-content::-webkit-scrollbar-track {
background: #f1f3f5;
}
.tabbar-scroll-content::-webkit-scrollbar-thumb {
background: #c9d6e4;
border-radius: 3px;
}
.tabbar-scroll-content::-webkit-scrollbar-thumb:hover {
background: #a8b9ca;
}
.tabbar-scroll-btn {
display: grid;
width: 28px;
flex: 0 0 auto;
place-items: center;
border: 0;
border-right: 1px solid #e4eaf0;
color: #6b7c8f;
background: #f7f9fb;
cursor: pointer;
z-index: 1;
transition: background 0.15s ease;
}
.tabbar-scroll-btn--right {
border-right: 0;
border-left: 1px solid #e4eaf0;
}
.tabbar-scroll-btn:hover {
background: #edf1f5;
color: #3d4c5c;
}
.tabbar-scroll-btn--left {
transform: rotate(180deg);
} }
.editor-tab { .editor-tab {
position: relative; position: relative;
display: flex; display: flex;
min-width: 190px; min-width: 100px;
max-width: 290px; max-width: 290px;
align-items: center; align-items: center;
gap: 8px; gap: 6px;
padding: 0 11px; padding: 0 6px;
border-right: 1px solid #e4eaf0; border-right: 1px solid #e4eaf0;
color: #4b5c70; color: #4b5c70;
background: #fff; background: #fff;
cursor: pointer;
font-size: 12px; font-size: 12px;
flex-shrink: 0;
}
.editor-tab:hover {
background: #f0f4f8;
} }
.editor-tab--active::before { .editor-tab--active::before {