feat(桌面与监控): 完善工作台导航和登录活动定位
Client Quality Gates / Shared client and Web (push) Has been cancelled
Client Quality Gates / macOS Desktop (push) Has been cancelled

- 优化桌面标签、上下文标题、前进后退、导航栏隐藏和原生菜单体验

- 补充登录会话 IP 采集、地理位置回退、管理端展示及数据库迁移

- 更新桌面发布检查、运维文档和前后端测试覆盖
This commit is contained in:
Cheng Zhou
2026-07-13 16:03:20 +08:00
parent b26ebdda02
commit ab59476d10
50 changed files with 2086 additions and 191 deletions
+105 -15
View File
@@ -37,6 +37,8 @@ describe("desktop layout shell", () => {
expect(source).toContain("<DesktopLayout v-if=\"isDesktop\" />");
expect(source).toContain("<WebLayout v-else />");
expect(app).toContain("const isDesktopRuntime = isTauriRuntime();");
expect(app).toContain("initializeDesktopThemePreference();");
expect(app).toContain("initializeDesktopMenuShortcutSync();");
expect(app).toContain('document.body.classList.add("is-desktop-runtime")');
expect(app).toContain('document.body.classList.remove("is-desktop-runtime")');
});
@@ -98,9 +100,14 @@ describe("desktop layout shell", () => {
expect(source).toContain("desktopToolbarSubtitle");
expect(source).not.toContain("desktopBreadcrumbs");
expect(source).not.toContain("breadcrumb-chip");
expect(source).not.toContain("history-controls");
expect(source).not.toContain('title="后退"');
expect(source).not.toContain('title="前进"');
expect(source).toContain('class="workspace-history-controls"');
expect(source).toContain('title="后退(当前任务)"');
expect(source).toContain('title="前进(当前任务)"');
expect(source).toContain(':disabled="!canNavigateWorkspaceTabBack"');
expect(source).toContain(':disabled="!canNavigateWorkspaceTabForward"');
expect(source).toContain('class="toolbar-history-divider"');
expect(source).toContain('v-for="(item, index) in desktopToolbarContextItems"');
expect(source).toContain('class="toolbar-context-link"');
expect(source).not.toContain('title="刷新当前视图"');
expect(source).not.toContain("const sidebarTitle");
expect(source).not.toContain('title="搜索命令"');
@@ -135,6 +142,29 @@ describe("desktop layout shell", () => {
expect(layout).not.toContain(["@tauri-apps", "api", "window"].join("/"));
});
it("uses a native macOS application menu and rebuilds a closed main window from the Dock", () => {
const tauriLib = readTauriLibSource();
expect(tauriLib.indexOf("package.name.clone()")).toBeLessThan(tauriLib.indexOf('"文件"'));
expect(tauriLib).toContain('Some("关于 CTMS")');
expect(tauriLib).toContain("short_version: Some(String::new())");
expect(tauriLib).toContain("icon: handle.default_window_icon().cloned()");
expect(tauriLib).toContain('"ctms.desktop.preferences"');
expect(tauriLib).toContain("PredefinedMenuItem::services");
expect(tauriLib).toContain("PredefinedMenuItem::hide_others");
expect(tauriLib).toContain("PredefinedMenuItem::show_all");
expect(tauriLib).toContain("WINDOW_SUBMENU_ID");
expect(tauriLib).toContain("HELP_SUBMENU_ID");
expect(tauriLib).toContain("TOGGLE_SIDEBAR_COMMAND_ID");
expect(tauriLib).toContain('"显示/隐藏导航栏"');
expect(tauriLib).toContain("RunEvent::Reopen");
expect(tauriLib).toContain("WebviewWindowBuilder::from_config");
expect(tauriLib).toContain('find(|config| config.label == "main")');
expect(tauriLib).not.toContain("WindowEvent::CloseRequested");
expect(tauriLib).not.toContain("api.prevent_close()");
expect(tauriLib).not.toContain("window.hide()");
});
it("keeps the desktop sidebar dense enough for navigation-heavy screens", () => {
const layout = readDesktopLayoutSource();
const sidebarHeadStart = layout.indexOf('<header class="sidebar-head">');
@@ -169,6 +199,24 @@ describe("desktop layout shell", () => {
expect(layout).not.toContain("padding: 44px 12px 12px;");
});
it("persists a fully hidden desktop sidebar with a macOS-safe restore control", () => {
const layout = readDesktopLayoutSource();
expect(layout).toContain("const desktopSidebarVisible = ref(readDesktopSidebarVisible());");
expect(layout).toContain("desktopSidebarVisible.value = setDesktopSidebarVisible(!desktopSidebarVisible.value);");
expect(layout).toContain("'is-sidebar-hidden': !desktopSidebarVisible");
expect(layout).toContain("'is-macos': desktopMetadata.platform === 'macos'");
expect(layout).toContain('class="toolbar-nav-button sidebar-toggle-button"');
expect(layout).toContain(':title="desktopSidebarToggleLabel"');
expect(layout).toContain('<Fold v-if="desktopSidebarVisible" /><Expand v-else />');
expect(layout).toContain('command === "ctms.desktop.toggleSidebar"');
expect(layout).toContain(".desktop-workbench.is-sidebar-hidden {");
expect(layout).toContain("grid-template-columns: 0 minmax(0, 1fr);");
expect(layout).toContain(".desktop-workbench.is-macos.is-sidebar-hidden .desktop-toolbar {");
expect(layout).toContain("padding-left: 88px;");
expect(layout).toContain("pointer-events: none;");
});
it("renders desktop preferences as a split settings panel", () => {
const preferences = readDesktopPreferencesSource();
const desktopLayout = readDesktopLayoutSource();
@@ -183,6 +231,10 @@ describe("desktop layout shell", () => {
expect(preferences).toContain("scrollbar-gutter: stable;");
expect(preferences).toContain('{ id: "connection", label: "连接"');
expect(preferences).toContain('{ id: "appearance", label: "外观"');
expect(preferences).toContain('{ value: "system" as const, label: "跟随系统"');
expect(preferences).toContain("grid-template-columns: repeat(3, minmax(0, 1fr));");
expect(preferences).toContain("width: min(100%, 336px);");
expect(preferences).toContain("white-space: nowrap;");
expect(preferences).toContain('{ id: "shortcuts", label: "快捷键"');
expect(preferences).toContain('{ id: "notifications", label: "通知"');
expect(preferences).toContain('{ id: "updates", label: "更新"');
@@ -249,7 +301,7 @@ describe("desktop layout shell", () => {
it("keeps workspace tabs stable and draggable like browser tabs", () => {
const source = readDesktopLayoutSource();
const tabsStart = source.indexOf('<div v-if="workspaceTabs.length" ref="workspaceTabsElement" class="workspace-tabs">');
const tabsStart = source.indexOf('<div v-if="workspaceTabs.length > 1" class="workspace-tabs">');
const tabsEnd = source.indexOf('<main class="desktop-content"', tabsStart);
const tabsTemplate = source.slice(tabsStart, tabsEnd);
@@ -292,6 +344,8 @@ describe("desktop layout shell", () => {
expect(source).toContain("replaceWorkspaceTabRoute(current.path, fullPath);");
expect(source).toContain("currentWorkspaceTabPath(workspaceTabHistories.value[tabPath], tabPath)");
expect(source).toContain("moveWorkspaceTabHistory(workspaceTabHistories.value[tabPath], offset)");
expect(source).toContain("canMoveWorkspaceTabHistory(activeWorkspaceTabHistory.value, -1)");
expect(source).toContain("canMoveWorkspaceTabHistory(activeWorkspaceTabHistory.value, 1)");
expect(source).toContain("navigateBack: () => navigateCurrentWorkspaceTabHistory(-1)");
expect(source).toContain("navigateForward: () => navigateCurrentWorkspaceTabHistory(1)");
expect(source).toContain('id: "desktop:tab-back"');
@@ -300,6 +354,25 @@ describe("desktop layout shell", () => {
expect(source).not.toContain("navigateForward: () => router.forward()");
});
it("updates a detail workspace tab after its business context has loaded", () => {
const source = readDesktopLayoutSource();
expect(source).toContain("buildWorkspaceTabTitle,");
expect(source).toContain("buildWorkspaceToolbarTitle,");
expect(source).toContain("resolveWorkspaceTabContextTitle,");
expect(source).toContain('} from "./layout/workspaceTabTitle";');
expect(source).toContain("const workspaceTabContexts = ref<Record<string, WorkspaceTabContext>>({});");
expect(source).toContain("title: buildWorkspaceTabTitle({");
expect(source).toContain("const pageTitle = resolveWorkspaceTabContextTitle({");
expect(source).toContain("() => [study.viewContext?.pageTitle, study.viewContext?.objectType] as const,");
expect(source).toContain("[activeMenu.value]: {");
expect(source).toContain("([pageTitle, objectType]) => {");
expect(source).toContain("if (current) addWorkspaceTab(current);");
expect(source).toContain('{ flush: "sync" },');
expect(source).toContain("if (!previousRoute || previousRoute.path !== route.path) {");
expect(source).toContain("const removeWorkspaceTabContext = (tabPath: string) => {");
});
it("allows desktop shortcuts to be customized from system preferences", () => {
const preferences = readDesktopPreferencesSource();
const tauriLib = readTauriLibSource();
@@ -313,30 +386,45 @@ describe("desktop layout shell", () => {
expect(preferences).toContain('window.addEventListener("pointerdown", cancelDesktopShortcutCaptureOnPointerDown, { capture: true })');
expect(preferences).toContain("event.stopImmediatePropagation()");
expect(preferences).toContain("恢复默认");
expect(tauriLib).not.toContain('Some("CmdOrCtrl+R")');
expect(tauriLib).not.toContain('Some("CmdOrCtrl+[")');
expect(tauriLib).not.toContain('Some("CmdOrCtrl+]")');
expect(tauriLib).toContain('Some("CmdOrCtrl+R")');
expect(tauriLib).toContain('Some("CmdOrCtrl+[")');
expect(tauriLib).toContain('Some("CmdOrCtrl+]")');
expect(tauriLib).toContain("desktop_menu_set_shortcuts");
expect(tauriLib).toContain("set_menu_accelerator");
});
it("moves overflowing workspace tabs into a fixed dropdown without horizontal scrolling", () => {
it("shows the four most recently opened tasks with a searchable all-tasks menu after five tabs", () => {
const source = readDesktopLayoutSource();
const tabsStart = source.indexOf('<div v-if="workspaceTabs.length" ref="workspaceTabsElement" class="workspace-tabs">');
const tabsStart = source.indexOf('<div v-if="workspaceTabs.length > 1" class="workspace-tabs">');
const tabsEnd = source.indexOf('<main class="desktop-content"', tabsStart);
const tabsTemplate = source.slice(tabsStart, tabsEnd);
expect(tabsTemplate).toContain('v-for="item in visibleWorkspaceTabs"');
expect(tabsTemplate).toContain('v-if="overflowWorkspaceTabs.length"');
expect(tabsTemplate).toContain('v-if="hasWorkspaceTabsOverflow"');
expect(tabsTemplate).toContain('popper-class="workspace-tabs-overflow-popper"');
expect(tabsTemplate).toContain('{{ overflowWorkspaceTabs.length }}');
expect(tabsTemplate).toContain('<span>全部任务</span>');
expect(tabsTemplate).toContain('{{ workspaceTabs.length }}');
expect(tabsTemplate).toContain('@command="navigateOverflowWorkspaceTab"');
expect(tabsTemplate).toContain('gridTemplateColumns: `repeat(${visibleWorkspaceTabs.length}, minmax(0, 238px))`');
expect(tabsTemplate).toContain('class="workspace-tabs-overflow-header"');
expect(source).toContain("const visibleWorkspaceTabCapacity = computed(() => {");
expect(tabsTemplate).toContain('v-model="workspaceTabsSearchQuery"');
expect(tabsTemplate).toContain('v-for="item in filteredWorkspaceTabs"');
expect(tabsTemplate).toContain('@click.stop="closeOtherWorkspaceTabs(activeMenu)"');
expect(tabsTemplate).toContain('@click.stop="closeAllWorkspaceTabs"');
expect(source).toContain("const DESKTOP_WORKSPACE_TABS_OVERFLOW_THRESHOLD = 5;");
expect(source).toContain("const DESKTOP_WORKSPACE_TABS_RECENT_VISIBLE_COUNT = 4;");
expect(source).toContain("const workspaceTabsByRecentUse = computed(() =>");
expect(source).toContain("const visibleWorkspaceTabs = computed(() => {");
expect(source).toContain("const overflowWorkspaceTabs = computed(() => {");
expect(source).toContain('workspaceTabsResizeObserver ||= new ResizeObserver(updateWorkspaceTabsAvailableWidth);');
expect(source).toContain("const DESKTOP_WORKSPACE_TABS_OVERFLOW_WIDTH = 106;");
expect(source).toContain("const recentVisiblePaths = new Set(");
expect(source).toContain("workspaceTabsByRecentUse.value.slice(-DESKTOP_WORKSPACE_TABS_RECENT_VISIBLE_COUNT)");
expect(source).toContain("return workspaceTabs.value.filter((item) => recentVisiblePaths.has(item.path));");
expect(source).toContain("const filteredWorkspaceTabs = computed(() => {");
expect(source).toContain("const closeAllWorkspaceTabs = () => {");
expect(source).not.toContain("visibleWorkspaceTabCapacity");
expect(source).not.toContain("workspaceTabsResizeObserver");
expect(source).toContain(".workspace-tabs-list {\n display: grid;");
expect(source).toContain("padding: 0 10px;");
expect(source).toContain("padding: 0 14px;");
expect(source).toContain("overflow: hidden;");
expect(source).not.toContain("overflow-x: auto;");
});
@@ -584,6 +672,8 @@ describe("desktop layout shell", () => {
expect(profile).toContain("overflow: hidden;");
expect(profile).not.toContain("overflow: auto;");
expect(profile).toContain("overflow-wrap: anywhere;");
expect(profile).toContain(".dialog-close:hover");
expect(profile).toContain(".dialog-close :deep(.el-icon)");
expect(preferences).toContain("flex-wrap: wrap;");
expect(preferences).toContain("overflow-wrap: anywhere;");
});