From 81ffe644460d9711408f7bcaf69112d0cd5bdd3f Mon Sep 17 00:00:00 2001 From: yanweidong Date: Sat, 25 Jul 2026 20:14:58 +0800 Subject: [PATCH] fix(desktop): lock appbar to edge --- apps/desktop/scripts/visual-check.mjs | 15 +++---- apps/desktop/src-tauri/src/lib.rs | 41 -------------------- apps/desktop/src-tauri/src/windows_appbar.rs | 24 ++---------- apps/desktop/src/App.tsx | 31 +-------------- apps/desktop/src/styles.css | 20 ++-------- 5 files changed, 13 insertions(+), 118 deletions(-) diff --git a/apps/desktop/scripts/visual-check.mjs b/apps/desktop/scripts/visual-check.mjs index 5c1eac0..4310a4b 100644 --- a/apps/desktop/scripts/visual-check.mjs +++ b/apps/desktop/scripts/visual-check.mjs @@ -63,31 +63,27 @@ const offlineConnection = await page.locator('.mini-connection').evaluate((eleme text: element.textContent, borderColor: getComputedStyle(element).borderColor, })) -await page.screenshot({ path: 'test-results/mini-login.png', fullPage: true }) +await page.screenshot({ path: 'test-results/app-login.png', fullPage: true }) const loginDefaults = await page.locator('.login-note input').evaluateAll((inputs) => inputs.map((input) => input.value)) await page.locator('.login-note input').nth(2).press('Enter') await page.waitForSelector('.mini-shell') await page.waitForTimeout(400) -await page.screenshot({ path: 'test-results/mini-home-right.png', fullPage: true }) +await page.screenshot({ path: 'test-results/app-home.png', fullPage: true }) const home = await page.evaluate(() => ({ width: document.querySelector('.mini-shell')?.getBoundingClientRect().width, navCount: document.querySelectorAll('.mini-nav button').length, - dock: document.documentElement.dataset.dock, overflowX: document.documentElement.scrollWidth > document.documentElement.clientWidth, title: document.querySelector('.page-intro h4')?.textContent ?? '', })) -await page.getByRole('button', { name: '停靠左侧' }).click() -const leftDock = await page.evaluate(() => document.documentElement.dataset.dock) - await page.getByRole('button', { name: '计划', exact: true }).click() await page.getByRole('button', { name: '设计', exact: true }).click() const tasks = await page.evaluate(() => ({ count: document.querySelectorAll('.task-note').length, title: document.querySelector('.task-note strong')?.textContent ?? '', })) -await page.screenshot({ path: 'test-results/mini-tasks.png', fullPage: true }) +await page.screenshot({ path: 'test-results/app-tasks.png', fullPage: true }) await page.getByRole('button', { name: '资料', exact: true }).click() const notes = await page.locator('.source-note').count() @@ -96,7 +92,7 @@ await page.getByRole('button', { name: 'AI', exact: true }).click() await page.getByRole('button', { name: /产品经理/ }).click() await page.locator('.mini-composer textarea').fill('请给出下一步计划') const sendEnabled = await page.getByRole('button', { name: '开始会话' }).isEnabled() -await page.screenshot({ path: 'test-results/mini-ai.png', fullPage: true }) +await page.screenshot({ path: 'test-results/app-ai.png', fullPage: true }) await browser.close() await server.close() @@ -107,12 +103,11 @@ if (loginDefaults[1] !== 'demo@senlin.ai' || loginDefaults[2] !== 'password123') if (errors.length) failures.push(`console errors: ${errors.join('; ')}`) if (home.width !== 420) failures.push(`expected 420px shell, got ${home.width}`) if (home.navCount !== 5) failures.push(`expected five top navigation items, got ${home.navCount}`) -if (home.dock !== 'right' || leftDock !== 'left') failures.push(`dock state failed: right=${home.dock}, left=${leftDock}`) if (home.overflowX) failures.push('mini client has horizontal overflow') if (home.title !== '森林项目') failures.push(`project overview missing, got ${home.title}`) if (tasks.count !== 1 || tasks.title !== '检查迷你布局') failures.push(`task tag filtering failed: ${JSON.stringify(tasks)}`) if (notes !== 2) failures.push(`expected two note cards, got ${notes}`) if (!sendEnabled) failures.push('AI composer must enable after selecting an expert and entering a prompt') -console.log(JSON.stringify({ home, leftDock, tasks, notes, sendEnabled, errors }, null, 2)) +console.log(JSON.stringify({ home, tasks, notes, sendEnabled, errors }, null, 2)) if (failures.length) throw new Error(failures.join('\n')) diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 6ae22c9..f04df89 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -1,49 +1,9 @@ -use tauri::Window; - #[cfg(windows)] use tauri::Manager; -#[cfg(not(windows))] -use tauri::{PhysicalPosition, PhysicalSize}; #[cfg(windows)] mod windows_appbar; -#[tauri::command] -fn dock_window(window: Window, side: String) -> Result<(), String> { - #[cfg(windows)] - { - let hwnd = window.hwnd().map_err(|error| error.to_string())?; - return windows_appbar::set_edge(hwnd, side == "left").map_err(|error| error.to_string()); - } - - #[cfg(not(windows))] - { - let monitor = window - .current_monitor() - .map_err(|error| error.to_string())? - .ok_or_else(|| "Unable to identify the current monitor".to_string())?; - let window_size = window.outer_size().map_err(|error| error.to_string())?; - let work_area = monitor.work_area(); - let dock_size = PhysicalSize::new(window_size.width, work_area.size.height); - let x = if side == "left" { - work_area.position.x - } else { - work_area.position.x + work_area.size.width as i32 - dock_size.width as i32 - }; - - window - .set_size(dock_size) - .map_err(|error| error.to_string())?; - window - .set_position(PhysicalPosition::new(x, work_area.position.y)) - .map_err(|error| error.to_string())?; - window - .set_always_on_top(true) - .map_err(|error| error.to_string())?; - Ok(()) - } -} - #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() @@ -58,7 +18,6 @@ pub fn run() { Ok(()) }) - .invoke_handler(tauri::generate_handler![dock_window]) .build(tauri::generate_context!()) .expect("failed to build SenlinAI App") .run(|_, event| { diff --git a/apps/desktop/src-tauri/src/windows_appbar.rs b/apps/desktop/src-tauri/src/windows_appbar.rs index d58ea4a..9998519 100644 --- a/apps/desktop/src-tauri/src/windows_appbar.rs +++ b/apps/desktop/src-tauri/src/windows_appbar.rs @@ -12,7 +12,7 @@ use windows::{ UI::{ Shell::{ DefSubclassProc, RemoveWindowSubclass, SHAppBarMessage, SetWindowSubclass, - ABE_LEFT, ABE_RIGHT, ABM_ACTIVATE, ABM_NEW, ABM_QUERYPOS, ABM_REMOVE, ABM_SETPOS, + ABE_RIGHT, ABM_ACTIVATE, ABM_NEW, ABM_QUERYPOS, ABM_REMOVE, ABM_SETPOS, ABM_WINDOWPOSCHANGED, ABN_POSCHANGED, APPBARDATA, }, WindowsAndMessaging::{ @@ -27,7 +27,6 @@ const SUBCLASS_ID: usize = 0x5341_4142; // "SAAB" (SenlinAI AppBar) static CALLBACK_MESSAGE: AtomicU32 = AtomicU32::new(0); static REGISTERED_HWND: AtomicIsize = AtomicIsize::new(0); -static EDGE: AtomicU32 = AtomicU32::new(ABE_RIGHT); static LAYOUT_IN_PROGRESS: AtomicBool = AtomicBool::new(false); /// Registers the main App window as a right-edge AppBar. @@ -65,12 +64,6 @@ pub fn register(hwnd: HWND) -> Result<()> { Ok(()) } -/// Repositions the AppBar after the App UI selects the left or right edge. -pub fn set_edge(hwnd: HWND, left: bool) -> Result<()> { - EDGE.store(if left { ABE_LEFT } else { ABE_RIGHT }, Ordering::Release); - layout(hwnd) -} - /// Removes the AppBar reservation before the Tauri window is destroyed. pub fn unregister() { let raw_hwnd = REGISTERED_HWND.swap(0, Ordering::AcqRel); @@ -137,23 +130,14 @@ fn layout_inner(hwnd: HWND) -> Result<()> { }; unsafe { GetMonitorInfoW(monitor, &mut monitor_info).ok()? }; - let edge = EDGE.load(Ordering::Acquire); let mut data = appbar_data(hwnd); - data.uEdge = edge; + data.uEdge = ABE_RIGHT; data.rc = monitor_info.rcMonitor; - if edge == ABE_LEFT { - data.rc.right = data.rc.left + width; - } else { - data.rc.left = data.rc.right - width; - } + data.rc.left = data.rc.right - width; unsafe { SHAppBarMessage(ABM_QUERYPOS, &mut data); - if edge == ABE_LEFT { - data.rc.right = data.rc.left + width; - } else { - data.rc.left = data.rc.right - width; - } + data.rc.left = data.rc.right - width; SHAppBarMessage(ABM_SETPOS, &mut data); SetWindowPos( hwnd, diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index b9f4590..bf47886 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -1,7 +1,7 @@ import { useEffect, useRef, useState } from 'react' import { Alert, Avatar, Button, Empty, Input, Message, Modal, Select, Spin, Tag, Typography } from '@arco-design/web-react' import { - IconApps, IconArrowLeft, IconArrowRight, IconCheckCircle, IconClockCircle, + IconApps, IconCheckCircle, IconClockCircle, IconFile, IconFolder, IconPlus, IconRefresh, IconRobot, IconSearch, IconSettings, } from '@arco-design/web-react/icon' import { @@ -12,7 +12,6 @@ import { const { Text, Title } = Typography type View = 'home' | 'tasks' | 'notes' | 'ai' | 'more' -type DockSide = 'left' | 'right' type Action = 'project' | 'task' | 'cron' | null type ConnectionStatus = 'checking' | 'online' | 'offline' @@ -32,21 +31,11 @@ export function App() { const [experts, setExperts] = useState([]) const [aiSessions, setAISessions] = useState([]) const [view, setView] = useState('home') - const [dockSide, setDockSide] = useState(() => { - if ('__TAURI_INTERNALS__' in window) return 'right' - return localStorage.getItem('senlin-mini-dock') === 'left' ? 'left' : 'right' - }) const [loading, setLoading] = useState(Boolean(session)) const [error, setError] = useState('') const [action, setAction] = useState(null) const refreshGeneration = useRef(0) - useEffect(() => { - document.documentElement.dataset.dock = dockSide - localStorage.setItem('senlin-mini-dock', dockSide) - void moveDesktopWindow(dockSide) - }, [dockSide]) - useEffect(() => { if (!session) return let cancelled = false @@ -89,10 +78,6 @@ export function App() { } } - async function dock(side: DockSide) { - setDockSide(side) - } - if (!session) return { localStorage.setItem('senlin-mini-session', JSON.stringify(next)); setSession(next) }} /> const activeProject = projects.find((project) => project.id === projectId) ?? projects[0] @@ -112,10 +97,6 @@ export function App() { 森林AI App 1.0 -
-
@@ -415,13 +396,3 @@ function errorMessage(error: unknown) { if (error instanceof ApiError || error instanceof Error) return error.message return '操作失败,请稍后重试' } - -async function moveDesktopWindow(side: DockSide) { - if (!('__TAURI_INTERNALS__' in window)) return - try { - const { invoke } = await import('@tauri-apps/api/core') - await invoke('dock_window', { side }) - } catch (requestError) { - Message.warning(errorMessage(requestError)) - } -} diff --git a/apps/desktop/src/styles.css b/apps/desktop/src/styles.css index 31bcda8..b7ad72e 100644 --- a/apps/desktop/src/styles.css +++ b/apps/desktop/src/styles.css @@ -17,19 +17,16 @@ button, input, textarea { font: inherit; } button { color: inherit; } .mini-shell { - width: min(100%, 440px); + width: 100%; height: 100vh; display: grid; grid-template-rows: 44px 50px 58px minmax(0, 1fr) 28px; overflow: hidden; background: var(--canvas); - border: 1px solid rgba(78, 89, 105, 0.18); - box-shadow: 0 20px 60px rgba(29, 33, 41, 0.18); + border: 0; + box-shadow: none; } -html[data-dock="right"] .mini-shell { margin-left: auto; } -html[data-dock="left"] .mini-shell { margin-right: auto; } - .mini-header, .project-switcher, .mini-nav, @@ -47,7 +44,6 @@ html[data-dock="left"] .mini-shell { margin-right: auto; } } .mini-brand, -.window-actions, .project-trigger, .mini-footer, .section-title, @@ -63,9 +59,6 @@ html[data-dock="left"] .mini-shell { margin-right: auto; } .mini-brand { gap: 7px; font-weight: 800; } .mini-brand img { width: 25px; height: 25px; } .mini-brand .arco-tag { margin-left: 2px; } -.window-actions { gap: 2px; } -.window-actions .arco-btn { color: #6b778c; } -.window-actions .active { color: var(--blue); background: #edf3ff; } .project-switcher { display: grid; @@ -236,14 +229,7 @@ html[data-dock="left"] .mini-shell { margin-right: auto; } .mini-form { display: grid; gap: 13px; } .mini-form .arco-select { width: 100%; } -@media (min-width: 700px) { - body { display: flex; } - html[data-dock="right"] body { justify-content: flex-end; } - html[data-dock="left"] body { justify-content: flex-start; } -} - @media (max-width: 359px) { - .mini-shell { border: 0; } .mini-brand .arco-tag { display: none; } .note-grid, .expert-strip { grid-template-columns: 1fr; } }