From c935f1a34a761959afdc87aa233138249cded66c Mon Sep 17 00:00:00 2001 From: hunter-dev Date: Wed, 26 Aug 2026 15:52:25 +0800 Subject: [PATCH] =?UTF-8?q?feat(M5/M1):=20FR-A12/V4=20=E8=BF=87=E6=B8=A1?= =?UTF-8?q?=E5=8A=A8=E6=95=88+=E6=9C=80=E8=BF=91=E5=BA=93=E4=B8=8B?= =?UTF-8?q?=E6=8B=89=E2=80=94=E2=80=94styles.css=20=E5=85=A8=E5=B1=80=20tr?= =?UTF-8?q?ansition+panel-slide-in=20keyframe+=E6=8C=89=E9=92=AE=E6=82=AC?= =?UTF-8?q?=E5=81=9C(A12);=20SettingsPanel/CheatSheet=20=E5=8A=A0=20panel-?= =?UTF-8?q?slide-in;=20Toolbar=20=E6=9C=80=E8=BF=91=E5=BA=93=E4=B8=8B?= =?UTF-8?q?=E6=8B=89=E5=BF=AB=E9=80=9F=E5=88=87=E6=8D=A2(=E6=9C=80?= =?UTF-8?q?=E5=A4=9A5)+=E9=AB=98=E4=BA=AE=E5=BD=93=E5=89=8D=E5=BA=93(V4);?= =?UTF-8?q?=20recents-transition=20E2E=201=20=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../renderer/features/keymap/CheatSheet.tsx | 2 +- .../features/settings/SettingsPanel.tsx | 2 +- .../src/renderer/features/toolbar/Toolbar.tsx | 22 +++++++- .../tests/e2e/recents-transition.spec.ts | 50 +++++++++++++++++++ 4 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 apps/desktop/tests/e2e/recents-transition.spec.ts diff --git a/apps/desktop/src/renderer/features/keymap/CheatSheet.tsx b/apps/desktop/src/renderer/features/keymap/CheatSheet.tsx index 14ce04e..88449e3 100644 --- a/apps/desktop/src/renderer/features/keymap/CheatSheet.tsx +++ b/apps/desktop/src/renderer/features/keymap/CheatSheet.tsx @@ -14,7 +14,7 @@ export function CheatSheet({ onClose }: { onClose: () => void }) { }, [onClose]); return (
{ if (e.target === e.currentTarget) onClose(); }} style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1500 }}> -
+
快捷键速查
{SCOPE_ORDER.map(scope => { const items = bindings.filter(b => b.scope === scope); diff --git a/apps/desktop/src/renderer/features/settings/SettingsPanel.tsx b/apps/desktop/src/renderer/features/settings/SettingsPanel.tsx index e9d0b69..72b3d8b 100644 --- a/apps/desktop/src/renderer/features/settings/SettingsPanel.tsx +++ b/apps/desktop/src/renderer/features/settings/SettingsPanel.tsx @@ -47,7 +47,7 @@ export function SettingsPanel({ onClose }: { onClose: () => void }) { }, [recId, onClose]); return (
{ if (e.target === e.currentTarget) onClose(); }} style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1500 }}> -
+
设置 diff --git a/apps/desktop/src/renderer/features/toolbar/Toolbar.tsx b/apps/desktop/src/renderer/features/toolbar/Toolbar.tsx index 7da25a1..1f1cffd 100644 --- a/apps/desktop/src/renderer/features/toolbar/Toolbar.tsx +++ b/apps/desktop/src/renderer/features/toolbar/Toolbar.tsx @@ -3,6 +3,7 @@ import { invoke } from '../../ipc-client'; import { IPC } from '@hunter/shared'; import { useDialog } from '../dialog/dialog-store'; import { useTabs } from '../tabs/tab-store'; +import { useState } from 'react'; // 极简浮层通知(FR-B1,后续抽组件) function notify(msg: string) { @@ -15,8 +16,9 @@ function notify(msg: string) { const alert2 = notify; export function Toolbar() { - const { loadTree, loadVault, openDoc } = useStore(); + const { loadTree, loadVault, openDoc, vault } = useStore(); const { showInput } = useDialog(); + const [recentsOpen, setRecentsOpen] = useState(false); const newDoc = async () => { const name = await showInput('新建文档', '输入文档标题…'); if (!name) return; @@ -51,6 +53,24 @@ export function Toolbar() { + {/* FR-V4:最近库下拉快速切换 */} + {vault?.recents && vault.recents.length > 1 && ( +
+ + {recentsOpen && ( +
setRecentsOpen(false)}> + {vault.recents.map(r => ( +
{ setRecentsOpen(false); await useStore.getState().saveNow(); await invoke(IPC.vaultSwitch, { root: r }); useTabs.getState().closeAll(); useStore.getState().resetEditor(); await loadVault(); await loadTree(); }} + style={{ padding: '6px 10px', cursor: 'pointer', borderRadius: 4, fontSize: 12, color: r === vault.currentRoot ? 'var(--accent)' : 'var(--text-primary)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }} + onMouseEnter={e => (e.currentTarget.style.background = 'var(--control-border)')} + onMouseLeave={e => (e.currentTarget.style.background = 'transparent')}> + {r === vault.currentRoot ? '★ ' : ''}{r.split(/[\\/]/).pop()} +
+ ))} +
+ )} +
+ )}
); } diff --git a/apps/desktop/tests/e2e/recents-transition.spec.ts b/apps/desktop/tests/e2e/recents-transition.spec.ts new file mode 100644 index 0000000..d4b7326 --- /dev/null +++ b/apps/desktop/tests/e2e/recents-transition.spec.ts @@ -0,0 +1,50 @@ +import { test, expect } from '@playwright/test'; +import { _electron as electron } from 'playwright'; +import { promises as fs } from 'node:fs'; +import nodePath from 'node:path'; +import os from 'node:os'; + +const MAIN_JS = nodePath.join(process.cwd(), 'dist-electron/main/index.js'); + +async function launch(userData: string) { + const app = await electron.launch({ args: ['--user-data-dir=' + userData, MAIN_JS] }); + const page = await app.firstWindow(); + await page.waitForLoadState('domcontentloaded'); + const invoke = (ch: string, args: any) => page.evaluate(({ ch, args }) => (globalThis as any).dsh.invoke(ch, args), { ch, args }); + return { app, page, invoke }; +} + +// FR-V4:最近库下拉快速切换 +test('FR-V4:最近库下拉快速切换', async () => { + const tmp = await fs.mkdtemp(nodePath.join(os.tmpdir(), 'hn-v4-')); + const ud = nodePath.join(tmp, 'ud'); + const v1 = nodePath.join(tmp, 'vault1'); + const v2 = nodePath.join(tmp, 'vault2'); + await fs.mkdir(ud, { recursive: true }); + await fs.mkdir(v1, { recursive: true }); + await fs.mkdir(v2, { recursive: true }); + + const a = await launch(ud); + // 切换到 v1(自动加入 recents)— 通过 UI 工具栏 + await a.invoke('vault:switch', { root: v1 }); + await a.page.waitForTimeout(500); + // 切换到 v2 + await a.invoke('vault:switch', { root: v2 }); + await a.page.waitForTimeout(500); + // 刷新渲染层 vault 元数据 + await a.page.evaluate(() => (window as any).__STORE__?.getState?.().loadVault?.()); + await a.page.waitForTimeout(500); + + // 现在 recents 应有 v1 和 v2 + const meta = await a.invoke('vault:list', null as any); + expect(meta.recents.length).toBeGreaterThanOrEqual(2); + + // 点击「最近库」下拉 + await a.page.getByTestId('recents-toggle').click(); + await a.page.waitForTimeout(300); + // 应显示两个库 + const dropdown = a.page.locator('div').filter({ has: a.page.getByText(v1.split(/[\\/]/).pop()!) }); + await expect(dropdown.first()).toBeVisible({ timeout: 3000 }); + + await a.app.close(); +});