优化桌面端标签导航与后台交互
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
<div class="sidebar-title-row">
|
||||
<h1>{{ TEXT.common.appName }}</h1>
|
||||
|
||||
<button v-if="!study.currentStudy" class="study-switcher-trigger empty" type="button" @click="openDesktopProjectEntry">
|
||||
<button v-if="showSidebarProjectChooser" class="study-switcher-trigger empty" type="button" @click="openDesktopProjectEntry">
|
||||
选择项目
|
||||
</button>
|
||||
</div>
|
||||
@@ -269,26 +269,87 @@
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="workspace-tabs" v-if="workspaceTabs.length">
|
||||
<div v-if="workspaceTabs.length" ref="workspaceTabsElement" class="workspace-tabs">
|
||||
<div
|
||||
v-for="item in workspaceTabs"
|
||||
:key="item.path"
|
||||
class="workspace-tab"
|
||||
:class="{ active: activeMenu === item.path, dragging: draggingWorkspaceTabPath === item.path }"
|
||||
draggable="true"
|
||||
@dragstart="startWorkspaceTabDrag(item.path, $event)"
|
||||
@dragover="allowWorkspaceTabDrop"
|
||||
@drop.prevent="dropWorkspaceTab(item.path, $event)"
|
||||
@dragend="draggingWorkspaceTabPath = ''"
|
||||
@contextmenu.prevent.stop="openWorkspaceTabMenu(item, $event)"
|
||||
class="workspace-tabs-list"
|
||||
:style="{ gridTemplateColumns: `repeat(${visibleWorkspaceTabs.length}, minmax(0, 238px))` }"
|
||||
>
|
||||
<button class="workspace-tab-action" type="button" @click="navigateWorkspaceTab(item.path)">
|
||||
<span>{{ item.title }}</span>
|
||||
</button>
|
||||
<button class="tab-close" type="button" title="关闭标签" @click.stop="closeWorkspaceTab(item.path)">
|
||||
×
|
||||
</button>
|
||||
<div
|
||||
v-for="item in visibleWorkspaceTabs"
|
||||
:key="item.path"
|
||||
class="workspace-tab"
|
||||
:class="{ active: activeMenu === item.path, dragging: draggingWorkspaceTabPath === item.path }"
|
||||
draggable="true"
|
||||
@dragstart="startWorkspaceTabDrag(item.path, $event)"
|
||||
@dragover="allowWorkspaceTabDrop"
|
||||
@drop.prevent="dropWorkspaceTab(item.path, $event)"
|
||||
@dragend="draggingWorkspaceTabPath = ''"
|
||||
@contextmenu.prevent.stop="openWorkspaceTabMenu(item, $event)"
|
||||
>
|
||||
<button
|
||||
class="workspace-tab-action"
|
||||
type="button"
|
||||
:title="item.title"
|
||||
@click="navigateWorkspaceTab(item.path)"
|
||||
>
|
||||
<span>{{ item.title }}</span>
|
||||
</button>
|
||||
<button class="tab-close" type="button" title="关闭标签" @click.stop="closeWorkspaceTab(item.path)">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-dropdown
|
||||
v-if="overflowWorkspaceTabs.length"
|
||||
class="workspace-tabs-overflow"
|
||||
popper-class="workspace-tabs-overflow-popper"
|
||||
trigger="click"
|
||||
placement="bottom-end"
|
||||
@command="navigateOverflowWorkspaceTab"
|
||||
@visible-change="workspaceTabsOverflowOpen = $event"
|
||||
>
|
||||
<button
|
||||
class="workspace-tabs-overflow-trigger"
|
||||
:class="{ 'is-open': workspaceTabsOverflowOpen }"
|
||||
type="button"
|
||||
:aria-label="`查看其余 ${overflowWorkspaceTabs.length} 个标签`"
|
||||
>
|
||||
<span>更多</span>
|
||||
<span class="workspace-tabs-overflow-count">{{ overflowWorkspaceTabs.length }}</span>
|
||||
<el-icon><ArrowDown /></el-icon>
|
||||
</button>
|
||||
<template #dropdown>
|
||||
<div class="workspace-tabs-overflow-menu">
|
||||
<div class="workspace-tabs-overflow-header">
|
||||
<span>其他标签</span>
|
||||
<span>{{ overflowWorkspaceTabs.length }} 个</span>
|
||||
</div>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item
|
||||
v-for="item in overflowWorkspaceTabs"
|
||||
:key="item.path"
|
||||
:command="item.path"
|
||||
:class="{ 'is-current': activeMenu === item.path }"
|
||||
>
|
||||
<span class="workspace-tabs-overflow-item-mark" aria-hidden="true"></span>
|
||||
<span class="workspace-tabs-overflow-label">
|
||||
<span :title="item.title">{{ item.title }}</span>
|
||||
</span>
|
||||
<button
|
||||
class="workspace-tabs-overflow-close"
|
||||
type="button"
|
||||
:aria-label="`关闭标签:${item.title}`"
|
||||
title="关闭标签"
|
||||
@click.stop="closeWorkspaceTab(item.path)"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</div>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
|
||||
<main class="desktop-content">
|
||||
@@ -415,13 +476,17 @@ import {
|
||||
} from "../session/desktopActivityCenter";
|
||||
import {
|
||||
DESKTOP_SERVER_URL_CHANGED_EVENT,
|
||||
DESKTOP_SHORTCUTS_CHANGED_EVENT,
|
||||
clearLegacyDesktopRouteHistoryPreference,
|
||||
formatDesktopShortcut,
|
||||
getAppMetadata,
|
||||
getDesktopServerUrl,
|
||||
listenDesktopMenuCommand,
|
||||
readDesktopFavoriteRoutes,
|
||||
readDesktopShortcutPreferences,
|
||||
toggleDesktopFavoriteRoute,
|
||||
type DesktopRoutePreference,
|
||||
type DesktopShortcutPreferences,
|
||||
} from "../runtime";
|
||||
import { dispatchDesktopRefreshCurrentView } from "../composables/useDesktopRefresh";
|
||||
import { useDesktopShortcuts } from "../composables/useDesktopShortcuts";
|
||||
@@ -438,6 +503,14 @@ import {
|
||||
type LayoutNavigationIcon,
|
||||
type LayoutNavigationItem,
|
||||
} from "./layout/navigation";
|
||||
import {
|
||||
createWorkspaceTabHistory,
|
||||
currentWorkspaceTabPath,
|
||||
moveWorkspaceTabHistory,
|
||||
recordWorkspaceTabPath,
|
||||
replaceWorkspaceTabPath,
|
||||
type WorkspaceTabHistory,
|
||||
} from "./layout/workspaceTabHistory";
|
||||
|
||||
const auth = useAuthStore();
|
||||
const study = useStudyStore();
|
||||
@@ -445,6 +518,10 @@ const router = useRouter();
|
||||
const route = useRoute();
|
||||
const desktopMetadata = getAppMetadata();
|
||||
const DESKTOP_WORKSPACE_TAB_CACHE_MAX = 12;
|
||||
const DESKTOP_WORKSPACE_TAB_MIN_WIDTH = 130;
|
||||
const DESKTOP_WORKSPACE_TAB_GAP = 6;
|
||||
const DESKTOP_WORKSPACE_TABS_HORIZONTAL_PADDING = 20;
|
||||
const DESKTOP_WORKSPACE_TABS_OVERFLOW_WIDTH = 106;
|
||||
const DESKTOP_ADMIN_SECTION_LABEL = "系统管理";
|
||||
const isAdmin = computed(() => !!auth.user?.is_admin);
|
||||
const projectRole = computed(() => study.currentStudyRole || (study.currentStudy as any)?.role_in_study || "");
|
||||
@@ -453,6 +530,7 @@ const canAccessProjectPath = (path: string) =>
|
||||
hasProjectPermission(study.currentPermissions, projectRole.value, getProjectRoutePermission(path), isAdmin.value);
|
||||
const hasAnyProjectModuleAccess = computed(() => projectRouteLandingPaths.some((path) => canAccessProjectPath(path)));
|
||||
const isAdminContext = computed(() => route.path.startsWith("/admin"));
|
||||
const showSidebarProjectChooser = computed(() => !study.currentStudy && !isAdminContext.value);
|
||||
const loggingOut = ref(false);
|
||||
const commandPaletteVisible = ref(false);
|
||||
const desktopPreferencesVisible = ref(false);
|
||||
@@ -462,9 +540,15 @@ const desktopServerUrl = ref(getDesktopServerUrl());
|
||||
const desktopUpdateStatus = ref<DesktopUpdateStatusSnapshot>(getDesktopUpdateStatus());
|
||||
const desktopActivities = ref<DesktopActivityItem[]>(getDesktopActivities());
|
||||
const desktopFavoriteRoutes = ref<DesktopRoutePreference[]>(readDesktopFavoriteRoutes());
|
||||
const desktopShortcutPreferences = ref<DesktopShortcutPreferences>(readDesktopShortcutPreferences());
|
||||
const workspaceTabs = ref<DesktopRoutePreference[]>([]);
|
||||
const workspaceTabHistories = ref<Record<string, WorkspaceTabHistory>>({});
|
||||
const workspaceTabsElement = ref<HTMLElement | null>(null);
|
||||
const workspaceTabsAvailableWidth = ref(0);
|
||||
const workspaceTabsOverflowOpen = ref(false);
|
||||
const draggingWorkspaceTabPath = ref("");
|
||||
const lastClosedWorkspaceTab = ref<DesktopRoutePreference | null>(null);
|
||||
const lastClosedWorkspaceTabHistory = ref<WorkspaceTabHistory | null>(null);
|
||||
const workspaceTabMenu = ref<{
|
||||
visible: boolean;
|
||||
x: number;
|
||||
@@ -477,11 +561,13 @@ const workspaceTabMenu = ref<{
|
||||
tab: null,
|
||||
});
|
||||
const expandedNavigationGroups = ref<Set<string>>(new Set());
|
||||
const collapsedNavigationGroups = ref<Set<string>>(new Set());
|
||||
const headerRemindersLoading = ref(false);
|
||||
const headerReminderStats = ref({ overdueAes: 0, overdueMonitoringIssues: 0 });
|
||||
let desktopMenuUnlisten: (() => void) | undefined;
|
||||
let desktopUpdateStatusUnlisten: (() => void) | undefined;
|
||||
let desktopActivityUnlisten: (() => void) | undefined;
|
||||
let workspaceTabsResizeObserver: ResizeObserver | undefined;
|
||||
|
||||
const iconComponentMap = {
|
||||
audit: Document,
|
||||
@@ -507,17 +593,23 @@ const activeMenu = computed(() => getActiveLayoutPath(route.path));
|
||||
const navigationGroupKey = (item: LayoutNavigationItem) => `${item.group}:${item.label}:${item.path}`;
|
||||
const isNavigationGroupActive = (item: LayoutNavigationItem) =>
|
||||
Boolean(item.children?.some((child) => activeMenu.value === child.path));
|
||||
const isNavigationGroupExpanded = (item: LayoutNavigationItem) =>
|
||||
isNavigationGroupActive(item) || expandedNavigationGroups.value.has(navigationGroupKey(item));
|
||||
const isNavigationGroupExpanded = (item: LayoutNavigationItem) => {
|
||||
const key = navigationGroupKey(item);
|
||||
return expandedNavigationGroups.value.has(key) || (isNavigationGroupActive(item) && !collapsedNavigationGroups.value.has(key));
|
||||
};
|
||||
const toggleNavigationGroup = (item: LayoutNavigationItem) => {
|
||||
const key = navigationGroupKey(item);
|
||||
const next = new Set(expandedNavigationGroups.value);
|
||||
if (next.has(key)) {
|
||||
next.delete(key);
|
||||
const nextExpanded = new Set(expandedNavigationGroups.value);
|
||||
const nextCollapsed = new Set(collapsedNavigationGroups.value);
|
||||
if (isNavigationGroupExpanded(item)) {
|
||||
nextExpanded.delete(key);
|
||||
nextCollapsed.add(key);
|
||||
} else {
|
||||
next.add(key);
|
||||
nextExpanded.add(key);
|
||||
nextCollapsed.delete(key);
|
||||
}
|
||||
expandedNavigationGroups.value = next;
|
||||
expandedNavigationGroups.value = nextExpanded;
|
||||
collapsedNavigationGroups.value = nextCollapsed;
|
||||
};
|
||||
const adminNavigationItems = computed(() =>
|
||||
study.currentStudy
|
||||
@@ -540,6 +632,10 @@ const projectDesktopNavigationItems = computed(() => flattenLayoutNavigationItem
|
||||
const desktopNavigationItems = computed(() =>
|
||||
[...adminDesktopNavigationItems.value, ...projectDesktopNavigationItems.value],
|
||||
);
|
||||
const workspaceTabFallbackPath = computed(() => {
|
||||
if (isAdminContext.value) return adminDesktopNavigationItems.value[0]?.path || "/admin/projects";
|
||||
return study.currentStudy ? "/project/overview" : "/desktop/project-entry";
|
||||
});
|
||||
|
||||
const userDisplayName = computed(
|
||||
() => auth.user?.full_name || auth.user?.username || auth.user?.email || TEXT.common.labels.userFallback,
|
||||
@@ -595,6 +691,35 @@ const currentDesktopRoutePreference = computed(() => {
|
||||
if (!item) return null;
|
||||
return { path: item.path, title: item.label, group: item.group };
|
||||
});
|
||||
const visibleWorkspaceTabCapacity = computed(() => {
|
||||
const tabCount = workspaceTabs.value.length;
|
||||
const availableWidth = workspaceTabsAvailableWidth.value;
|
||||
if (!tabCount || availableWidth <= 0) return tabCount;
|
||||
const maxWithoutOverflow = Math.max(
|
||||
1,
|
||||
Math.floor((availableWidth + DESKTOP_WORKSPACE_TAB_GAP) / (DESKTOP_WORKSPACE_TAB_MIN_WIDTH + DESKTOP_WORKSPACE_TAB_GAP)),
|
||||
);
|
||||
if (tabCount <= maxWithoutOverflow) return tabCount;
|
||||
return Math.max(
|
||||
1,
|
||||
Math.floor(
|
||||
(availableWidth - DESKTOP_WORKSPACE_TABS_OVERFLOW_WIDTH) /
|
||||
(DESKTOP_WORKSPACE_TAB_MIN_WIDTH + DESKTOP_WORKSPACE_TAB_GAP),
|
||||
),
|
||||
);
|
||||
});
|
||||
const visibleWorkspaceTabs = computed(() => {
|
||||
const capacity = visibleWorkspaceTabCapacity.value;
|
||||
if (workspaceTabs.value.length <= capacity) return workspaceTabs.value;
|
||||
const firstTabs = workspaceTabs.value.slice(0, capacity);
|
||||
const activeTab = workspaceTabs.value.find((item) => item.path === activeMenu.value);
|
||||
if (!activeTab || firstTabs.some((item) => item.path === activeTab.path)) return firstTabs;
|
||||
return [...firstTabs.slice(0, -1), activeTab];
|
||||
});
|
||||
const overflowWorkspaceTabs = computed(() => {
|
||||
const visiblePaths = new Set(visibleWorkspaceTabs.value.map((item) => item.path));
|
||||
return workspaceTabs.value.filter((item) => !visiblePaths.has(item.path));
|
||||
});
|
||||
const currentRouteFavorited = computed(() => {
|
||||
const current = currentDesktopRoutePreference.value;
|
||||
return Boolean(current && desktopFavoriteRoutes.value.some((item) => item.path === current.path));
|
||||
@@ -673,11 +798,64 @@ const addWorkspaceTab = (routePreference: Pick<DesktopRoutePreference, "path" |
|
||||
workspaceTabs.value = [...workspaceTabs.value, item];
|
||||
};
|
||||
|
||||
const setWorkspaceTabHistory = (tabPath: string, history: WorkspaceTabHistory) => {
|
||||
workspaceTabHistories.value = { ...workspaceTabHistories.value, [tabPath]: history };
|
||||
};
|
||||
|
||||
const removeWorkspaceTabHistory = (tabPath: string) => {
|
||||
if (!workspaceTabHistories.value[tabPath]) return;
|
||||
const next = { ...workspaceTabHistories.value };
|
||||
delete next[tabPath];
|
||||
workspaceTabHistories.value = next;
|
||||
};
|
||||
|
||||
const recordWorkspaceTabRoute = (tabPath: string, fullPath: string) => {
|
||||
setWorkspaceTabHistory(tabPath, recordWorkspaceTabPath(workspaceTabHistories.value[tabPath], fullPath));
|
||||
};
|
||||
|
||||
const replaceWorkspaceTabRoute = (tabPath: string, fullPath: string) => {
|
||||
setWorkspaceTabHistory(tabPath, replaceWorkspaceTabPath(workspaceTabHistories.value[tabPath], fullPath));
|
||||
};
|
||||
|
||||
const workspaceTabDestination = (tabPath: string) =>
|
||||
currentWorkspaceTabPath(workspaceTabHistories.value[tabPath], tabPath);
|
||||
|
||||
const desktopRouteCacheKey = (currentRoute: { fullPath: string }) => currentRoute.fullPath;
|
||||
|
||||
const navigateWorkspaceTab = (path: string) => {
|
||||
if (activeMenu.value === path) return;
|
||||
void router.push(path);
|
||||
void router.push(workspaceTabDestination(path));
|
||||
};
|
||||
|
||||
const navigateCurrentWorkspaceTabHistory = (offset: -1 | 1) => {
|
||||
const tabPath = activeMenu.value;
|
||||
const movement = moveWorkspaceTabHistory(workspaceTabHistories.value[tabPath], offset);
|
||||
if (!movement) return;
|
||||
setWorkspaceTabHistory(tabPath, movement.history);
|
||||
void router.push(movement.path);
|
||||
};
|
||||
|
||||
const navigateOverflowWorkspaceTab = (path: string) => {
|
||||
navigateWorkspaceTab(path);
|
||||
};
|
||||
|
||||
const updateWorkspaceTabsAvailableWidth = () => {
|
||||
const element = workspaceTabsElement.value;
|
||||
workspaceTabsAvailableWidth.value = element
|
||||
? Math.max(0, element.clientWidth - DESKTOP_WORKSPACE_TABS_HORIZONTAL_PADDING)
|
||||
: 0;
|
||||
};
|
||||
|
||||
const observeWorkspaceTabsElement = (element: HTMLElement | null) => {
|
||||
workspaceTabsResizeObserver?.disconnect();
|
||||
if (!element) {
|
||||
workspaceTabsAvailableWidth.value = 0;
|
||||
return;
|
||||
}
|
||||
updateWorkspaceTabsAvailableWidth();
|
||||
if (typeof ResizeObserver === "undefined") return;
|
||||
workspaceTabsResizeObserver ||= new ResizeObserver(updateWorkspaceTabsAvailableWidth);
|
||||
workspaceTabsResizeObserver.observe(element);
|
||||
};
|
||||
|
||||
const closeWorkspaceTabMenu = () => {
|
||||
@@ -734,17 +912,21 @@ const dropWorkspaceTab = (targetPath: string, event: DragEvent) => {
|
||||
|
||||
const closeWorkspaceTab = (path: string) => {
|
||||
const index = workspaceTabs.value.findIndex((tab) => tab.path === path);
|
||||
if (index >= 0) lastClosedWorkspaceTab.value = workspaceTabs.value[index];
|
||||
if (index >= 0) {
|
||||
lastClosedWorkspaceTab.value = workspaceTabs.value[index];
|
||||
lastClosedWorkspaceTabHistory.value = workspaceTabHistories.value[path] || createWorkspaceTabHistory(path);
|
||||
}
|
||||
workspaceTabs.value = workspaceTabs.value.filter((tab) => tab.path !== path);
|
||||
removeWorkspaceTabHistory(path);
|
||||
if (activeMenu.value !== path) return;
|
||||
const next =
|
||||
workspaceTabs.value[index] ||
|
||||
workspaceTabs.value[index - 1] ||
|
||||
desktopFavoriteRoutes.value.find((item) => item.path !== path);
|
||||
if (next) {
|
||||
router.push(next.path);
|
||||
router.push(workspaceTabDestination(next.path));
|
||||
} else {
|
||||
router.push(study.currentStudy ? "/project/overview" : "/desktop/project-entry");
|
||||
router.push(workspaceTabFallbackPath.value);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -757,7 +939,9 @@ const closeOtherWorkspaceTabs = (path: string) => {
|
||||
const target = workspaceTabs.value.find((tab) => tab.path === path);
|
||||
if (!target) return;
|
||||
workspaceTabs.value = [target];
|
||||
if (activeMenu.value !== path) void router.push(path);
|
||||
const targetHistory = workspaceTabHistories.value[path];
|
||||
workspaceTabHistories.value = targetHistory ? { [path]: targetHistory } : {};
|
||||
if (activeMenu.value !== path) void router.push(workspaceTabDestination(path));
|
||||
closeWorkspaceTabMenu();
|
||||
};
|
||||
|
||||
@@ -765,8 +949,12 @@ const restoreLastClosedWorkspaceTab = () => {
|
||||
const tab = lastClosedWorkspaceTab.value;
|
||||
if (!tab) return;
|
||||
addWorkspaceTab(tab);
|
||||
void router.push(tab.path);
|
||||
if (lastClosedWorkspaceTabHistory.value) {
|
||||
setWorkspaceTabHistory(tab.path, lastClosedWorkspaceTabHistory.value);
|
||||
}
|
||||
void router.push(workspaceTabDestination(tab.path));
|
||||
lastClosedWorkspaceTab.value = null;
|
||||
lastClosedWorkspaceTabHistory.value = null;
|
||||
closeWorkspaceTabMenu();
|
||||
};
|
||||
|
||||
@@ -774,6 +962,11 @@ const updateDesktopServerUrl = () => {
|
||||
desktopServerUrl.value = getDesktopServerUrl();
|
||||
};
|
||||
|
||||
const updateDesktopShortcutPreferences = (event?: Event) => {
|
||||
desktopShortcutPreferences.value =
|
||||
(event as CustomEvent<DesktopShortcutPreferences> | undefined)?.detail || readDesktopShortcutPreferences();
|
||||
};
|
||||
|
||||
const openCommandPalette = () => {
|
||||
commandPaletteVisible.value = true;
|
||||
};
|
||||
@@ -785,7 +978,9 @@ const openDesktopPreferences = () => {
|
||||
const openDesktopProjectEntry = async () => {
|
||||
study.clearCurrentStudy();
|
||||
workspaceTabs.value = [];
|
||||
workspaceTabHistories.value = {};
|
||||
lastClosedWorkspaceTab.value = null;
|
||||
lastClosedWorkspaceTabHistory.value = null;
|
||||
await router.push("/desktop/project-entry");
|
||||
};
|
||||
|
||||
@@ -819,11 +1014,11 @@ const handleDesktopMenuCommand = (command: string) => {
|
||||
return;
|
||||
}
|
||||
if (command === "ctms.desktop.back") {
|
||||
router.back();
|
||||
navigateCurrentWorkspaceTabHistory(-1);
|
||||
return;
|
||||
}
|
||||
if (command === "ctms.desktop.forward") {
|
||||
router.forward();
|
||||
navigateCurrentWorkspaceTabHistory(1);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -847,11 +1042,27 @@ const desktopCommands = computed<DesktopCommand[]>(() => {
|
||||
run: () => navigateWorkspaceTab(item.path),
|
||||
}));
|
||||
return [
|
||||
{
|
||||
id: "desktop:tab-back",
|
||||
title: "当前标签后退",
|
||||
group: "标签页",
|
||||
shortcut: formatDesktopShortcut(desktopShortcutPreferences.value.back),
|
||||
visible: true,
|
||||
run: () => navigateCurrentWorkspaceTabHistory(-1),
|
||||
},
|
||||
{
|
||||
id: "desktop:tab-forward",
|
||||
title: "当前标签前进",
|
||||
group: "标签页",
|
||||
shortcut: formatDesktopShortcut(desktopShortcutPreferences.value.forward),
|
||||
visible: true,
|
||||
run: () => navigateCurrentWorkspaceTabHistory(1),
|
||||
},
|
||||
{
|
||||
id: "desktop:refresh",
|
||||
title: "刷新当前视图",
|
||||
group: "桌面",
|
||||
shortcut: "⌘R",
|
||||
shortcut: formatDesktopShortcut(desktopShortcutPreferences.value.refresh),
|
||||
visible: true,
|
||||
run: refreshCurrentDesktopView,
|
||||
},
|
||||
@@ -1009,12 +1220,20 @@ const beforeProfileDialogClose = async (done: () => void) => {
|
||||
};
|
||||
|
||||
watch(
|
||||
() => route.path,
|
||||
() => {
|
||||
() => route.fullPath,
|
||||
(fullPath, previousFullPath) => {
|
||||
study.setViewContext(null);
|
||||
const current = currentDesktopRoutePreference.value;
|
||||
if (current) {
|
||||
addWorkspaceTab(current);
|
||||
const previousTabPath = previousFullPath ? getActiveLayoutPath(router.resolve(previousFullPath).path) : "";
|
||||
const replacesCurrentTabEntry =
|
||||
previousTabPath === current.path && Boolean((window.history.state as { replaced?: boolean } | null)?.replaced);
|
||||
if (replacesCurrentTabEntry) {
|
||||
replaceWorkspaceTabRoute(current.path, fullPath);
|
||||
} else {
|
||||
recordWorkspaceTabRoute(current.path, fullPath);
|
||||
}
|
||||
refreshDesktopRoutePreferences();
|
||||
}
|
||||
},
|
||||
@@ -1033,12 +1252,17 @@ watch(
|
||||
() => refreshDesktopRoutePreferences(),
|
||||
);
|
||||
|
||||
watch(workspaceTabsElement, (element) => observeWorkspaceTabsElement(element), { flush: "post" });
|
||||
|
||||
onMounted(async () => {
|
||||
window.addEventListener(DESKTOP_SERVER_URL_CHANGED_EVENT, updateDesktopServerUrl);
|
||||
window.addEventListener(DESKTOP_SHORTCUTS_CHANGED_EVENT, updateDesktopShortcutPreferences);
|
||||
window.addEventListener("resize", updateWorkspaceTabsAvailableWidth);
|
||||
clearLegacyDesktopRouteHistoryPreference();
|
||||
const current = currentDesktopRoutePreference.value;
|
||||
if (current) {
|
||||
addWorkspaceTab(current);
|
||||
recordWorkspaceTabRoute(current.path, route.fullPath);
|
||||
}
|
||||
refreshDesktopRoutePreferences();
|
||||
desktopMenuUnlisten = await listenDesktopMenuCommand(handleDesktopMenuCommand).catch(() => undefined);
|
||||
@@ -1065,6 +1289,9 @@ onMounted(async () => {
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener(DESKTOP_SERVER_URL_CHANGED_EVENT, updateDesktopServerUrl);
|
||||
window.removeEventListener(DESKTOP_SHORTCUTS_CHANGED_EVENT, updateDesktopShortcutPreferences);
|
||||
window.removeEventListener("resize", updateWorkspaceTabsAvailableWidth);
|
||||
workspaceTabsResizeObserver?.disconnect();
|
||||
desktopMenuUnlisten?.();
|
||||
desktopUpdateStatusUnlisten?.();
|
||||
desktopActivityUnlisten?.();
|
||||
@@ -1089,8 +1316,8 @@ useDesktopShortcuts(
|
||||
}
|
||||
return false;
|
||||
},
|
||||
navigateBack: () => router.back(),
|
||||
navigateForward: () => router.forward(),
|
||||
navigateBack: () => navigateCurrentWorkspaceTabHistory(-1),
|
||||
navigateForward: () => navigateCurrentWorkspaceTabHistory(1),
|
||||
refreshCurrentView: refreshCurrentDesktopView,
|
||||
},
|
||||
);
|
||||
@@ -1772,22 +1999,31 @@ useDesktopShortcuts(
|
||||
|
||||
.workspace-tabs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
gap: 6px;
|
||||
padding: 7px 10px;
|
||||
overflow-x: auto;
|
||||
overflow: hidden;
|
||||
border-bottom: 1px solid #dfe7f0;
|
||||
background: #f4f7fa;
|
||||
}
|
||||
|
||||
.workspace-tabs-list {
|
||||
display: grid;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
gap: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.workspace-tab {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 150px;
|
||||
max-width: 238px;
|
||||
min-width: 0;
|
||||
height: 30px;
|
||||
box-sizing: border-box;
|
||||
padding: 0 5px 0 10px;
|
||||
border: 1px solid #d2dce8;
|
||||
border-radius: 8px;
|
||||
@@ -1848,6 +2084,172 @@ useDesktopShortcuts(
|
||||
color: #142033;
|
||||
}
|
||||
|
||||
.workspace-tabs-overflow {
|
||||
display: flex;
|
||||
flex: 0 0 106px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.workspace-tabs-overflow-trigger {
|
||||
appearance: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 106px;
|
||||
height: 30px;
|
||||
gap: 6px;
|
||||
padding: 0 9px 0 11px;
|
||||
border: 1px solid #c5d1df;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
|
||||
color: #3d5068;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.workspace-tabs-overflow-trigger:hover,
|
||||
.workspace-tabs-overflow-trigger:focus-visible,
|
||||
.workspace-tabs-overflow-trigger.is-open {
|
||||
border-color: #aebfd1;
|
||||
background: #eaf1f8;
|
||||
color: #183756;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.workspace-tabs-overflow-count {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 20px;
|
||||
height: 18px;
|
||||
box-sizing: border-box;
|
||||
padding: 0 6px;
|
||||
border-radius: 9px;
|
||||
background: #e3ebf4;
|
||||
color: #31506f;
|
||||
font-size: 10px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
:global(.workspace-tabs-overflow-popper.el-popper) {
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
border: 1px solid #d6e0eb;
|
||||
border-radius: 10px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 14px 38px rgba(15, 23, 42, 0.16), 0 2px 8px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
:global(.workspace-tabs-overflow-popper .el-popper__arrow::before) {
|
||||
border-color: #d6e0eb;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.workspace-tabs-overflow-menu {
|
||||
width: 252px;
|
||||
}
|
||||
|
||||
.workspace-tabs-overflow-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 38px;
|
||||
padding: 0 12px;
|
||||
border-bottom: 1px solid #e5ebf2;
|
||||
color: #2d4058;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.workspace-tabs-overflow-header span:last-child {
|
||||
color: #8292a6;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
:global(.workspace-tabs-overflow-popper .el-dropdown-menu) {
|
||||
min-width: 0;
|
||||
max-height: min(360px, calc(100vh - 120px));
|
||||
padding: 6px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
:global(.workspace-tabs-overflow-popper .el-dropdown-menu__item) {
|
||||
display: flex;
|
||||
min-height: 38px;
|
||||
gap: 9px;
|
||||
padding: 0 6px 0 10px;
|
||||
border-radius: 7px;
|
||||
color: #33485f;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
:global(.workspace-tabs-overflow-popper .el-dropdown-menu__item:hover),
|
||||
:global(.workspace-tabs-overflow-popper .el-dropdown-menu__item:focus) {
|
||||
background: #f0f5fa;
|
||||
color: #183756;
|
||||
}
|
||||
|
||||
:global(.workspace-tabs-overflow-popper .el-dropdown-menu__item.is-current) {
|
||||
background: #eaf1f8;
|
||||
color: #183756;
|
||||
}
|
||||
|
||||
.workspace-tabs-overflow-label {
|
||||
display: block;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.workspace-tabs-overflow-item-mark {
|
||||
flex: 0 0 7px;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: #9fb1c4;
|
||||
}
|
||||
|
||||
.workspace-tabs-overflow-label > span {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.workspace-tabs-overflow-close {
|
||||
appearance: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 24px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: #7b8da3;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
font-size: 16px;
|
||||
opacity: 0.62;
|
||||
}
|
||||
|
||||
.workspace-tabs-overflow-close:hover,
|
||||
.workspace-tabs-overflow-close:focus-visible {
|
||||
background: rgba(15, 23, 42, 0.08);
|
||||
color: #142033;
|
||||
opacity: 1;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.workspace-tab-context-menu {
|
||||
position: fixed;
|
||||
z-index: 2300;
|
||||
@@ -2007,7 +2409,8 @@ useDesktopShortcuts(
|
||||
:global([data-ctms-theme="dark"] .update-notice),
|
||||
:global([data-ctms-theme="dark"] .connection-button),
|
||||
:global([data-ctms-theme="dark"] .account-button),
|
||||
:global([data-ctms-theme="dark"] .workspace-tab) {
|
||||
:global([data-ctms-theme="dark"] .workspace-tab),
|
||||
:global([data-ctms-theme="dark"] .workspace-tabs-overflow-trigger) {
|
||||
border-color: #334155;
|
||||
background: #172033;
|
||||
color: #dbe5f1;
|
||||
@@ -2047,6 +2450,59 @@ useDesktopShortcuts(
|
||||
background: #111827;
|
||||
}
|
||||
|
||||
:global([data-ctms-theme="dark"] .workspace-tabs-overflow-count) {
|
||||
background: #243247;
|
||||
color: #bfdbfe;
|
||||
}
|
||||
|
||||
:global([data-ctms-theme="dark"] .workspace-tabs-overflow-popper .el-dropdown-menu) {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
:global([data-ctms-theme="dark"] .workspace-tabs-overflow-popper.el-popper) {
|
||||
border-color: #334155;
|
||||
background: #172033;
|
||||
}
|
||||
|
||||
:global([data-ctms-theme="dark"] .workspace-tabs-overflow-popper .el-popper__arrow::before) {
|
||||
border-color: #334155;
|
||||
background: #172033;
|
||||
}
|
||||
|
||||
:global([data-ctms-theme="dark"] .workspace-tabs-overflow-header) {
|
||||
border-bottom-color: #334155;
|
||||
color: #e5edf7;
|
||||
}
|
||||
|
||||
:global([data-ctms-theme="dark"] .workspace-tabs-overflow-header span:last-child) {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
:global([data-ctms-theme="dark"] .workspace-tabs-overflow-popper .el-dropdown-menu__item) {
|
||||
color: #dbe5f1;
|
||||
}
|
||||
|
||||
:global([data-ctms-theme="dark"] .workspace-tabs-overflow-popper .el-dropdown-menu__item:hover),
|
||||
:global([data-ctms-theme="dark"] .workspace-tabs-overflow-popper .el-dropdown-menu__item:focus),
|
||||
:global([data-ctms-theme="dark"] .workspace-tabs-overflow-popper .el-dropdown-menu__item.is-current) {
|
||||
background: #243247;
|
||||
color: #bfdbfe;
|
||||
}
|
||||
|
||||
:global([data-ctms-theme="dark"] .workspace-tabs-overflow-item-mark) {
|
||||
background: #6f87a1;
|
||||
}
|
||||
|
||||
:global([data-ctms-theme="dark"] .workspace-tabs-overflow-close) {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
:global([data-ctms-theme="dark"] .workspace-tabs-overflow-close:hover),
|
||||
:global([data-ctms-theme="dark"] .workspace-tabs-overflow-close:focus-visible) {
|
||||
background: rgba(226, 232, 240, 0.1);
|
||||
color: #f8fafc;
|
||||
}
|
||||
|
||||
:global([data-ctms-theme="dark"] .desktop-content) {
|
||||
background: #0f172a;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user