From c2639ca7af97d4ecb29b6ecc6859db9f0272ca7e Mon Sep 17 00:00:00 2001 From: GouJ Date: Wed, 9 Sep 2026 10:18:02 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=B8=BA=E6=A1=8C=E9=9D=A2=E6=8A=98?= =?UTF-8?q?=E5=8F=A0=E4=BE=A7=E6=A0=8F=E6=B7=BB=E5=8A=A0=E8=8F=9C=E5=8D=95?= =?UTF-8?q?=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PC 折叠侧栏菜单图标增加 NPopover 提示,并保留图标导航能力;展开态和移动端不显示重复提示。 --- design-qa.md | 61 +++++++++---------- src/App.vue | 97 +++++++++++++++++++++--------- tests/components/ui/layout.test.ts | 19 +++++- 3 files changed, 117 insertions(+), 60 deletions(-) diff --git a/design-qa.md b/design-qa.md index b18503e..74b0c63 100644 --- a/design-qa.md +++ b/design-qa.md @@ -1,61 +1,58 @@ # Design QA -- Source visual truth: `/workspace/scratch/6e52996114b8/upload/f7dbeb9e-a47c-4e09-9b5c-c5f885b0d708.png` -- Source pixel dimensions: `543 × 978`. -- Browser-rendered open-state comparison: `/workspace/scratch/mobile-sidebar-open-comparison-20260908.jpg` -- Browser-rendered closed-state comparison: `/workspace/scratch/mobile-sidebar-closed-comparison-20260908.jpg` -- Comparison screenshot dimensions: `1363 × 936`. -- Implementation CSS viewport: `543 × 936`, device scale factor `1`. -- Density normalization: source and implementation are both displayed at `543px` CSS width; the source's bottom `42px` is outside the comparison viewport and contains no sidebar structure needed for this review. -- State: dark-theme project list, with the mobile navigation captured both open and closed. +- Source visual truth: `/workspace/scratch/6e52996114b8/upload/6c29987c-43a6-4ded-ba86-b66d1c638f51.png` +- Source pixel dimensions: `151 × 506`. +- Browser-rendered implementation: `/workspace/scratch/6e52996114b8/sidebar-popover-pc-final-20260909.jpg` +- Browser-rendered implementation dimensions: `1363 × 936`. +- Focused side-by-side comparison: `/workspace/scratch/6e52996114b8/sidebar-popover-comparison-final-20260909.jpg` +- Implementation CSS viewport: `1363 × 936`, device scale factor `1`. +- State: PC dark theme, collapsed sidebar, pointer hovering the disabled “视觉风格” icon. ## Full-view comparison evidence -The source shows a permanently visible `48px` navigation rail that reduces the project-content width. The revised closed state removes that rail entirely: browser geometry reports the main region at `543px`, equal to the `543px` iframe viewport. The top-left control is now a menu icon and the project list uses the full available width. - -The revised open state shows the same navigation as a `543 × 936` fixed full-screen layer. Brand, navigation items and the bottom backend entry remain vertically separated, while an explicit close icon is available in the navigation header. +The implementation preserves the existing collapsed sidebar width, icon order, selected state and main-content boundary. Hovering an icon opens a compact Naive UI popover to the right without shifting the sidebar or content. ## Focused region comparison evidence -A separate crop was not needed because both states are rendered at 1:1 CSS width in the full-view comparison and the header controls, navigation item, content boundary and footer action remain readable. +The focused comparison places the supplied sidebar crop beside a `320 × 506` crop of the browser render. The source icon spacing, dark surfaces and green selected state remain unchanged. The implementation adds the requested hover label beside the icon; disabled workflow items use the explanatory suffix “(剧本完成后可用)”. ## Required fidelity surfaces -- Fonts and typography: existing font family, sizes, weights and truncation behavior are preserved. The drawer uses the same brand and menu typography as the desktop sidebar. -- Spacing and layout rhythm: the closed state removes the previous `48px` content offset. The open state uses the full viewport and preserves the existing header, menu and footer spacing. -- Colors and visual tokens: the drawer continues to use `--app-subtle`, existing selected-menu colors and the current dark-theme tokens. -- Image quality and asset fidelity: no raster assets were added or altered. Menu and close actions use the project's existing Lucide icon package. -- Copy and content: brand, “我的剧本” and “后端连接” labels are unchanged. +- Typography: the application typography is unchanged; popover copy uses the current Naive UI theme and compact UI text sizing. +- Spacing and layout rhythm: the `64px` collapsed sidebar and existing vertical menu rhythm are preserved. Popovers use `placement="right"` and do not alter document flow. +- Colors and visual tokens: dark sidebar surfaces and the selected green state remain unchanged. Popovers inherit the existing dark Naive UI theme. +- Image and asset fidelity: no raster assets were introduced. Existing Lucide menu icons are preserved. +- Copy and content: PC hints use each menu's existing label. Locked entries append “(剧本完成后可用)”; “后端连接” keeps its original copy. ## Findings - No actionable P0/P1/P2 visual or responsive issues remain. -- The disconnected local backend produces the expected in-page `502` status in the closed-state project list; it does not affect the navigation layout or interaction. +- The disconnected local backend produces the expected in-page `502` response; it does not affect sidebar layout or popover behavior. ## Primary interactions tested -- Opened the full-screen navigation from the new menu button. -- Closed the navigation from its internal close button. -- Confirmed the hidden navigation is removed from the accessibility tree with `aria-hidden` and `inert`. -- Confirmed the closed-state main content width equals the mobile viewport width. -- Confirmed desktop behavior remains covered by the existing layout component tests. +- Collapsed the PC sidebar and hovered an enabled menu icon; “我的剧本” appeared. +- Hovered the disabled “视觉风格” icon; its availability explanation appeared. +- Hovered the bottom settings icon; “后端连接” appeared. +- Clicked an enabled collapsed icon and confirmed navigation to the target workflow route. +- Expanded the PC sidebar and confirmed menu popovers were absent. +- Confirmed through component tests that mobile navigation does not render menu popovers. ## Console errors checked -No application-origin console errors were reported. The cloud browser logged only its own extension metadata messages; these are outside the application. +No application-origin console errors were reported. The cloud browser logged only extension metadata messages outside the application. ## Comparison history -- Iteration 1: replaced the persistent mobile rail with a fixed full-screen navigation and added the menu/close icon pair. -- Iteration 2: verified the open state at `543 × 936`, then closed it through the internal action and verified the content region expanded to the full `543px` viewport width. +- Iteration 1: added native NPopover wrappers and found that the full-width transparent RouterLink intercepted hover on enabled menu icons. +- Iteration 2: made enabled collapsed icons direct RouterLinks, raised their trigger layer above the transparent label and rechecked enabled, disabled and settings hints in the browser. ## Implementation checklist -- [x] Hide the mobile navigation without reserving horizontal space. -- [x] Open navigation as a full-screen fixed layer. -- [x] Replace the top toggle glyph with a menu icon. -- [x] Provide an internal close control while the top bar is covered. -- [x] Preserve automatic close on mobile route changes, settings open and Escape. -- [x] Preserve desktop collapse/expand behavior. +- [x] Add NPopover hints to all PC collapsed sidebar menu icons. +- [x] Preserve click navigation for enabled collapsed icons. +- [x] Explain locked workflow items in their hint copy. +- [x] Keep PC expanded and mobile navigation free of duplicate popovers. +- [x] Preserve sidebar size, spacing, theme and selected state. final result: passed diff --git a/src/App.vue b/src/App.vue index da63e63..7aa78f2 100644 --- a/src/App.vue +++ b/src/App.vue @@ -8,8 +8,8 @@ import { NLayout, NLayoutHeader, NMenu, + NPopover, NScrollbar, - NTooltip, zhCN, dateZhCN, type MenuOption @@ -49,6 +49,10 @@ const collapsed = ref(narrow.value) const settingsOpen = ref(false) /** 桌面折叠只裁切文字;移动端侧栏打开时始终显示完整菜单。 */ const menuCollapsed = false +/** 菜单内部会缓存图标 VNode,切换显示模式时重建一次以同步 Popover 状态。 */ +const menuRenderKey = computed( + () => `${narrow.value ? 'mobile' : 'desktop'}-${collapsed.value ? 'collapsed' : 'expanded'}` +) /** Naive Menu 的桌面折叠宽度;移动端覆盖层不参与正文宽度计算。 */ const siderCollapsedWidth = SIDER_COLLAPSED_WIDTH /** 侧栏展开宽度仅用于桌面布局。 */ @@ -111,31 +115,62 @@ const workflowItems = [ ['storyboard', '分镜设计', Camera], ['production', '镜头生产', Clapperboard] ] as const + +/** PC 折叠侧栏只显示图标时渲染 Popover;可用图标同时保留路由跳转能力。 */ +function createMenuIcon(icon: typeof FolderOpen, label: string, popoverEnabled: boolean, target?: string) { + return () => { + const trigger = () => + popoverEnabled && target + ? h(RouterLink, { to: target, class: 'admin-menu-popover-trigger' }, () => h(icon, { size: 18 })) + : h('span', { class: 'admin-menu-popover-trigger' }, [h(icon, { size: 18 })]) + if (!popoverEnabled) return trigger() + + return h( + NPopover, + { trigger: 'hover', placement: 'right' }, + { + trigger, + default: () => label + } + ) + } +} + /** 菜单链接使用 RouterLink,保留新标签打开和浏览器导航行为。 */ -const menuOptions = computed(() => [ - { - key: '/projects', - label: () => h(RouterLink, { to: '/projects' }, () => '我的剧本'), - icon: () => h(FolderOpen, { size: 18 }) - }, - ...(projectId.value - ? [ - { type: 'divider' as const, key: 'divider' }, - ...workflowItems.map(([path, label, icon]) => { - const disabled = path !== 'create-drama' && !workflowsUnlocked.value - return { - key: '/projects/' + projectId.value + '/' + path, - disabled, - label: () => - disabled - ? h('span', { title: '剧本完成后可用', 'aria-disabled': 'true' }, label) - : h(RouterLink, { to: '/projects/' + projectId.value + '/' + path }, () => label), - icon: () => h(icon, { size: 18 }) - } - }) - ] - : []) -]) +const menuOptions = computed(() => { + // 仅在 PC 折叠态创建 Popover,避免展开态和移动端出现重复提示。 + const popoverEnabled = !narrow.value && collapsed.value + return [ + { + key: '/projects', + label: () => h(RouterLink, { to: '/projects' }, () => '我的剧本'), + icon: createMenuIcon(FolderOpen, '我的剧本', popoverEnabled, '/projects') + }, + ...(projectId.value + ? [ + { type: 'divider' as const, key: 'divider' }, + ...workflowItems.map(([path, label, icon]) => { + const disabled = path !== 'create-drama' && !workflowsUnlocked.value + const target = '/projects/' + projectId.value + '/' + path + return { + key: target, + disabled, + label: () => + disabled + ? h('span', { title: '剧本完成后可用', 'aria-disabled': 'true' }, label) + : h(RouterLink, { to: target }, () => label), + icon: createMenuIcon( + icon, + disabled ? `${label}(剧本完成后可用)` : label, + popoverEnabled, + disabled ? undefined : target + ) + } + }) + ] + : []) + ] +})