性能优化

This commit is contained in:
zhongluofeng
2026-08-06 10:33:16 +08:00
parent c7578a2e6b
commit e66c53e66d
105 changed files with 7273 additions and 5002 deletions
+3 -1
View File
@@ -8,7 +8,9 @@ use std::path::PathBuf;
use serde::Serialize;
use walkdir::WalkDir;
#[derive(Debug, Clone, Serialize)]
use specta::Type;
#[derive(Debug, Clone, Serialize, Type)]
#[serde(rename_all = "camelCase")]
pub struct AppRecord {
pub name: String,
+63 -18
View File
@@ -7,21 +7,25 @@ use super::{file_index, app_scanner, icon_extractor};
/// 读取快速面板设置(快捷键等)
#[tauri::command]
#[specta::specta]
pub async fn quickpanel_get_settings(app: AppHandle) -> Result<QuickPanelSettings, String> {
Ok(popup::load_settings(&app))
}
/// 保存快速面板设置;快捷键变化时自动重新注册 + 预创建窗口
#[tauri::command]
#[specta::specta]
pub async fn quickpanel_save_settings(
settings: QuickPanelSettings,
app: AppHandle,
) -> Result<(), String> {
let prev_shortcut = popup::load_settings(&app).shortcut;
popup::save_settings(&app, &settings)?;
// 快捷键变化时重新注册
// 快捷键变化时重新注册(共享工具模块,原子化 + 冲突检测)
if settings.shortcut != prev_shortcut {
popup::register_shortcut(&app, &settings.shortcut)?;
crate::shortcut::register_shortcut(&app, "快速面板", &settings.shortcut, |a| {
popup::show_popup(a)
})?;
// 新快捷键非空时确保弹窗窗口已预创建
if !settings.shortcut.trim().is_empty() {
popup::ensure_window(&app);
@@ -32,22 +36,25 @@ pub async fn quickpanel_save_settings(
/// 注册(或切换)快速面板全局快捷键
#[tauri::command]
#[specta::specta]
pub async fn quickpanel_register_shortcut(
shortcut: String,
app: AppHandle,
) -> Result<(), String> {
popup::register_shortcut(&app, &shortcut)
crate::shortcut::register_shortcut(&app, "快速面板", &shortcut, |a| popup::show_popup(a))
}
/// 注销快速面板全局快捷键
#[tauri::command]
#[specta::specta]
pub async fn quickpanel_unregister_shortcut(app: AppHandle) -> Result<(), String> {
popup::unregister_shortcut(&app);
crate::shortcut::unregister_shortcut(&app, "快速面板");
Ok(())
}
/// 手动触发显示快速面板(供 UI 按钮调用)
#[tauri::command]
#[specta::specta]
pub async fn quickpanel_show_popup(app: AppHandle) -> Result<(), String> {
popup::show_popup(&app);
Ok(())
@@ -55,6 +62,7 @@ pub async fn quickpanel_show_popup(app: AppHandle) -> Result<(), String> {
/// 隐藏快速面板
#[tauri::command]
#[specta::specta]
pub async fn quickpanel_hide_popup(app: AppHandle) -> Result<(), String> {
popup::hide_popup(&app);
Ok(())
@@ -62,6 +70,7 @@ pub async fn quickpanel_hide_popup(app: AppHandle) -> Result<(), String> {
/// 显示已创建的弹窗窗口(前端 onMounted 后调用)
#[tauri::command]
#[specta::specta]
pub async fn quickpanel_show_window(app: AppHandle) -> Result<(), String> {
popup::show_window(&app);
Ok(())
@@ -69,6 +78,7 @@ pub async fn quickpanel_show_window(app: AppHandle) -> Result<(), String> {
/// 锁定屏幕(Windows: rundll32 user32.dll,LockWorkStationCREATE_NO_WINDOW 避免黑窗)
#[tauri::command]
#[specta::specta]
pub fn quickpanel_lock_screen() -> Result<(), String> {
#[cfg(windows)]
{
@@ -87,13 +97,16 @@ pub fn quickpanel_lock_screen() -> Result<(), String> {
/// 初始化文件索引数据库(应用启动时调用)
#[tauri::command]
pub fn quickpanel_init_file_index(app: AppHandle) -> Result<(), String> {
file_index::init(&app);
Ok(())
#[specta::specta]
pub async fn quickpanel_init_file_index(app: AppHandle) -> Result<(), String> {
tauri::async_runtime::spawn_blocking(move || file_index::init(&app))
.await
.map_err(|e| format!("索引初始化任务失败: {}", e))
}
/// 构建文件索引(全量重建,阻塞操作建议在 spawn_blocking 调用)
#[tauri::command]
#[specta::specta]
pub async fn quickpanel_build_file_index(app: AppHandle) -> Result<i64, String> {
let settings = popup::load_settings(&app);
let dirs = if settings.index_dirs.is_empty() {
@@ -107,33 +120,51 @@ pub async fn quickpanel_build_file_index(app: AppHandle) -> Result<i64, String>
.map_err(|e| format!("索引任务失败: {}", e))?
}
/// 搜索文件索引
/// 搜索文件索引SQLite 查询移出主线程)
#[tauri::command]
pub fn quickpanel_search_files(query: String, limit: Option<i64>) -> Vec<file_index::FileRecord> {
file_index::search(&query, limit.unwrap_or(50))
#[specta::specta]
pub async fn quickpanel_search_files(
query: String,
limit: Option<i64>,
) -> Result<Vec<file_index::FileRecord>, String> {
tauri::async_runtime::spawn_blocking(move || file_index::search(&query, limit.unwrap_or(50)))
.await
.map_err(|e| format!("搜索任务失败: {}", e))
}
/// 获取索引状态
#[tauri::command]
#[specta::specta]
pub fn quickpanel_file_index_stats() -> file_index::IndexStats {
file_index::stats()
}
/// 扫描已安装应用
/// 扫描已安装应用(遍历开始菜单/桌面/磁盘,移出主线程)
#[tauri::command]
pub fn quickpanel_scan_apps() -> Vec<app_scanner::AppRecord> {
app_scanner::scan_apps()
#[specta::specta]
pub async fn quickpanel_scan_apps() -> Result<Vec<app_scanner::AppRecord>, String> {
tauri::async_runtime::spawn_blocking(app_scanner::scan_apps)
.await
.map_err(|e| format!("扫描应用任务失败: {}", e))
}
/// 获取应用图标(data URL)。命中内存/磁盘缓存时零 Windows API 调用。
/// 前端按需为可见项调用,避免一次性加载全部图标。
/// 未命中缓存时 SHGetFileInfoW + 编码 + 落盘为阻塞操作,移出主线程。
#[tauri::command]
pub fn quickpanel_get_app_icon(app: AppHandle, path: String) -> Option<String> {
icon_extractor::get_icon_data_url(&app, &path)
#[specta::specta]
pub async fn quickpanel_get_app_icon(
app: AppHandle,
path: String,
) -> Result<Option<String>, String> {
tauri::async_runtime::spawn_blocking(move || icon_extractor::get_icon_data_url(&app, &path))
.await
.map_err(|e| format!("图标提取任务失败: {}", e))
}
/// 清理图标缓存(磁盘 + 内存)
#[tauri::command]
#[specta::specta]
pub fn quickpanel_clear_app_icon_cache(app: AppHandle) -> Result<(), String> {
icon_extractor::clear_cache(&app);
Ok(())
@@ -141,6 +172,7 @@ pub fn quickpanel_clear_app_icon_cache(app: AppHandle) -> Result<(), String> {
/// 在资源管理器中显示文件(选中)
#[tauri::command]
#[specta::specta]
pub fn quickpanel_reveal_in_explorer(path: String) -> Result<(), String> {
#[cfg(windows)]
{
@@ -192,18 +224,21 @@ pub fn quickpanel_reveal_in_explorer(path: String) -> Result<(), String> {
/// - 目录:explorer.exe 直接打开(修复索引目录点击后未打开的问题)
/// - 文件:ShellExecuteW open,无关联应用时自动 fallback 到「打开方式」对话框(verb: openas
#[tauri::command]
#[specta::specta]
pub fn quickpanel_open_file(path: String) -> Result<(), String> {
super::special_locations::open_path(&path)
}
/// 获取 Windows 常用快捷位置(hosts、回收站、此电脑、用户目录、系统管理工具等)
#[tauri::command]
#[specta::specta]
pub fn quickpanel_get_special_locations() -> Vec<super::special_locations::SpecialLocation> {
super::special_locations::get_special_locations()
}
/// 打开快捷位置(kind: file | shell | cmd
#[tauri::command]
#[specta::specta]
pub fn quickpanel_open_special(
kind: String,
target: String,
@@ -216,13 +251,21 @@ pub fn quickpanel_open_special(
}
}
/// 删除文件(移到回收站)
/// 删除文件(移到回收站PowerShell 阻塞等待移出主线程
#[tauri::command]
pub fn quickpanel_delete_file(path: String) -> Result<(), String> {
#[specta::specta]
pub async fn quickpanel_delete_file(path: String) -> Result<(), String> {
tauri::async_runtime::spawn_blocking(move || delete_file_impl(&path))
.await
.map_err(|e| format!("删除任务失败: {}", e))?
}
/// 删除文件实现:PowerShell + Microsoft.VisualBasic 移到回收站
fn delete_file_impl(path: &str) -> Result<(), String> {
#[cfg(windows)]
{
use crate::process_manager::setup_creation_flags;
let p = std::path::Path::new(&path);
let p = std::path::Path::new(path);
let is_dir = p.is_dir();
// 用 PowerShell + Microsoft.VisualBasic 移到回收站
let script = if is_dir {
@@ -258,6 +301,7 @@ pub fn quickpanel_delete_file(path: String) -> Result<(), String> {
/// 运行自定义命令(执行可执行文件 + 参数)
/// .lnk 快捷方式不能直接 spawnos error 193),需通过 cmd /C 启动
#[tauri::command]
#[specta::specta]
pub fn quickpanel_run_custom_command(command: String, args: Vec<String>) -> Result<(), String> {
use crate::process_manager::setup_creation_flags;
let is_lnk = command
@@ -282,6 +326,7 @@ pub fn quickpanel_run_custom_command(command: String, args: Vec<String>) -> Resu
/// 运行系统命令(不设置 CREATE_NO_WINDOW,使 cmd/powershell/regedit 等显示自身窗口)
/// 适用于内置系统工具:regedit、shutdown、cmd、powershell、taskmgr 等。
#[tauri::command]
#[specta::specta]
pub fn quickpanel_run_system_command(command: String, args: Vec<String>) -> Result<(), String> {
let mut cmd = std::process::Command::new(&command);
cmd.args(&args);
+19 -20
View File
@@ -13,8 +13,10 @@ use serde::Serialize;
use tauri::{AppHandle, Manager};
use walkdir::WalkDir;
use specta::Type;
/// 单个文件记录(返回给前端)
#[derive(Debug, Clone, Serialize)]
#[derive(Debug, Clone, Serialize, Type)]
#[serde(rename_all = "camelCase")]
pub struct FileRecord {
pub path: String,
@@ -25,7 +27,7 @@ pub struct FileRecord {
}
/// 索引状态(返回给前端)
#[derive(Debug, Clone, Serialize)]
#[derive(Debug, Clone, Serialize, Type)]
#[serde(rename_all = "camelCase")]
pub struct IndexStats {
pub total: i64,
@@ -61,7 +63,7 @@ pub fn init(app: &AppHandle) {
let conn = match Connection::open(&path) {
Ok(c) => c,
Err(e) => {
eprintln!("[quickpanel] 文件索引 DB 初始化失败: {}", e);
crate::logger::log_error("quickpanel", &format!("文件索引 DB 初始化失败: {}", e));
return;
}
};
@@ -84,11 +86,11 @@ pub fn init(app: &AppHandle) {
);",
);
let mut guard = index_slot().lock().unwrap();
let mut guard = index_slot().lock().unwrap_or_else(|e| e.into_inner());
*guard = Some(Inner {
conn: Mutex::new(conn),
});
eprintln!("[quickpanel] 文件索引 DB 已就绪: {}", path.display());
crate::logger::log_info("quickpanel", &format!("文件索引 DB 已就绪: {}", path.display()));
}
/// 判断索引是否已初始化
@@ -96,7 +98,7 @@ fn with_conn<F, R>(f: F) -> Option<R>
where
F: FnOnce(&Connection) -> R,
{
let guard = index_slot().lock().unwrap();
let guard = index_slot().lock().unwrap_or_else(|e| e.into_inner());
if let Some(inner) = guard.as_ref() {
if let Ok(conn) = inner.conn.lock() {
return Some(f(&conn));
@@ -140,7 +142,7 @@ pub fn build_index(dirs: &[String]) -> Result<i64, String> {
// 启动/刷新 notify 监听器
start_watcher(dirs);
eprintln!("[quickpanel] 文件索引完成,共 {}", count);
crate::logger::log_info("quickpanel", &format!("文件索引完成,共 {}", count));
Ok(count)
}
@@ -203,10 +205,13 @@ fn upsert_path(p: &Path) -> bool {
fn remove_path(p: &Path) {
let path_str = p.to_string_lossy().to_string();
let _ = with_conn(|conn| {
// 删除该路径及其子项(目录被删除时,子文件也失效)
// 删除该路径本身及其直接子项(目录被删除时,子文件也失效)
// 用"路径 + 分隔符"的前缀匹配(而非裸前缀),避免误删 dir2/directory 等兄弟目录。
let backslash_prefix = format!("{}\\{}", path_str, "%");
let slash_prefix = format!("{}/{}", path_str, "%");
conn.execute(
"DELETE FROM files WHERE path = ?1 OR path LIKE ?2",
params![path_str, format!("{}%", path_str)],
"DELETE FROM files WHERE path = ?1 OR path LIKE ?2 OR path LIKE ?3",
params![path_str, backslash_prefix, slash_prefix],
)
});
}
@@ -306,7 +311,7 @@ pub fn start_watcher(dirs: &[String]) {
) {
Ok(w) => w,
Err(e) => {
eprintln!("[quickpanel] notify watcher 创建失败: {}", e);
crate::logger::log_error("quickpanel", &format!("notify watcher 创建失败: {}", e));
return;
}
};
@@ -318,14 +323,14 @@ pub fn start_watcher(dirs: &[String]) {
continue;
}
if let Err(e) = watcher.watch(path, RecursiveMode::Recursive) {
eprintln!("[quickpanel] watch {} 失败: {}", dir, e);
crate::logger::log_error("quickpanel", &format!("watch {} 失败: {}", dir, e));
}
}
// 替换旧 watcherdrop 时自动 unwatch
let mut guard = watcher_slot().lock().unwrap();
let mut guard = watcher_slot().lock().unwrap_or_else(|e| e.into_inner());
*guard = Some(watcher);
eprintln!("[quickpanel] notify 监听已启动,监听 {} 个目录", dirs.len());
crate::logger::log_info("quickpanel", &format!("notify 监听已启动,监听 {} 个目录", dirs.len()));
}
/// 处理文件系统事件:创建/修改 → upsert,删除 → remove,重命名 → remove + upsert
@@ -348,9 +353,3 @@ fn handle_fs_event(event: &notify::Event) {
}
}
}
/// 停止 notify 监听器
pub fn stop_watcher() {
let mut guard = watcher_slot().lock().unwrap();
*guard = None;
}
+16 -10
View File
@@ -10,7 +10,7 @@
//! - 返回 base64 data URL 而非文件路径,避免独立弹窗窗口的 asset 协议配置问题
//! - 磁盘缓存避免重复 Windows API 调用(昂贵),内存缓存避免重复磁盘读取 + 编码
use std::collections::HashMap;
use std::collections::{HashMap, VecDeque};
use std::path::PathBuf;
use std::sync::Mutex;
@@ -18,26 +18,32 @@ use base64::Engine as _;
use tauri::{AppHandle, Manager};
// ===== 内存缓存 =====
static MEM_CACHE: Mutex<Option<HashMap<String, String>>> = Mutex::new(None);
/// (path → data URL) + FIFO 淘汰队列(队头最旧,超限时先淘汰)
static MEM_CACHE: Mutex<Option<(HashMap<String, String>, VecDeque<String>)>> = Mutex::new(None);
const MEM_CACHE_MAX: usize = 512;
fn mem_get(path: &str) -> Option<String> {
let cache = MEM_CACHE.lock().ok()?;
cache.as_ref()?.get(path).cloned()
cache.as_ref()?.0.get(path).cloned()
}
fn mem_put(path: String, url: String) {
if let Ok(mut guard) = MEM_CACHE.lock() {
let map = guard.get_or_insert_with(HashMap::new);
let slot = guard.get_or_insert_with(|| (HashMap::new(), VecDeque::new()));
let (map, order) = &mut *slot;
if map.contains_key(&path) {
// 已存在:仅更新值,不重复入队
map.insert(path, url);
return;
}
if map.len() >= MEM_CACHE_MAX {
// 简单清理:丢弃一半(最早插入的,HashMap 无序,近似随机)
let keep = map.len() / 2;
let keys: Vec<String> = map.keys().cloned().collect();
for k in keys.iter().skip(keep) {
map.remove(k);
// FIFO 淘汰最旧条目(O(1)),避免无序淘汰把刚插入的常用图标清掉
if let Some(oldest) = order.pop_front() {
map.remove(&oldest);
}
}
map.insert(path, url);
map.insert(path.clone(), url);
order.push_back(path);
}
}
+1 -1
View File
@@ -21,4 +21,4 @@ pub use commands::{
quickpanel_search_files, quickpanel_show_popup, quickpanel_show_window,
quickpanel_unregister_shortcut,
};
pub use popup::{ensure_window, load_settings, register_shortcut};
pub use popup::{ensure_window, load_settings};
+41 -76
View File
@@ -17,9 +17,10 @@ use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use tauri::{AppHandle, Emitter, Manager, WebviewUrl, WebviewWindowBuilder};
use tauri::window::{Effect, EffectsBuilder};
use tauri_plugin_global_shortcut::{GlobalShortcutExt, Shortcut, ShortcutState};
use crate::clipboard::popup::{get_cursor_pos, get_work_area_at_point, get_dpi_for_point};
use crate::win32_util::{get_cursor_pos, get_work_area_at_point, get_dpi_for_point};
use specta::Type;
/// 弹窗窗口标签
pub const POPUP_LABEL: &str = "quick-panel";
@@ -28,15 +29,16 @@ pub const POPUP_LABEL: &str = "quick-panel";
const WIN_W: f64 = 600.0;
const WIN_H: f64 = 420.0;
/// 当前注册的快捷键(用于注销旧快捷键)
static CURRENT_SHORTCUT: Mutex<Option<String>> = Mutex::new(None);
/// 标志:show_popup 兜底创建路径设为 true,前端 onMounted 回调 show_window 时据此判断是否显示。
/// 预创建路径不设置,避免应用启动时弹窗自动弹出。
static POPUP_PENDING_SHOW: AtomicBool = AtomicBool::new(false);
/// 兜底创建路径下 show_popup 计算出的待显示位置(物理坐标),供 show_window 应用,
/// 避免窗口重建后仍停留在屏幕外 (-10000, -10000)。
static PENDING_POS: Mutex<Option<(f64, f64)>> = Mutex::new(None);
/// 自定义命令
#[derive(Clone, Serialize, Deserialize)]
#[derive(Clone, Serialize, Deserialize, Type)]
#[serde(rename_all = "camelCase")]
pub struct CustomCommand {
pub id: String,
@@ -47,7 +49,7 @@ pub struct CustomCommand {
}
/// 快速面板设置
#[derive(Clone, Serialize, Deserialize)]
#[derive(Clone, Serialize, Deserialize, Type)]
#[serde(rename_all = "camelCase")]
pub struct QuickPanelSettings {
/// 全局快捷键(如 "Alt+Space"),空字符串表示不注册。
@@ -131,56 +133,6 @@ pub fn save_settings(app: &AppHandle, settings: &QuickPanelSettings) -> Result<(
std::fs::write(&path, json).map_err(|e| format!("写入设置文件失败: {}", e))
}
/// 解析快捷键字符串为 Shortcut(格式如 "Alt+Space"、"Ctrl+Shift+P"
/// 失败返回 None。
pub fn parse_shortcut(s: &str) -> Option<Shortcut> {
s.trim().parse::<Shortcut>().ok()
}
/// 注册全局快捷键。重复调用会先注销旧快捷键。
/// 传入空字符串则仅注销不注册。
pub fn register_shortcut(app: &AppHandle, shortcut_str: &str) -> Result<(), String> {
// 先注销旧快捷键
unregister_shortcut(app);
if shortcut_str.trim().is_empty() {
return Ok(());
}
let shortcut = parse_shortcut(shortcut_str)
.ok_or_else(|| format!("无效的快捷键: {}", shortcut_str))?;
let app_handle = app.clone();
app.global_shortcut()
.on_shortcut(shortcut, move |_app, _shortcut, event| {
// 仅在按下时触发(松开不触发)
if event.state == ShortcutState::Pressed {
show_popup(&app_handle);
}
})
.map_err(|e| format!("注册快捷键失败: {}", e))?;
if let Ok(mut cur) = CURRENT_SHORTCUT.lock() {
*cur = Some(shortcut_str.to_string());
}
eprintln!("[quickpanel] 已注册快捷键: {}", shortcut_str);
Ok(())
}
/// 注销当前快捷键
pub fn unregister_shortcut(app: &AppHandle) {
if let Ok(cur) = CURRENT_SHORTCUT.lock() {
if let Some(ref s) = *cur {
if let Some(shortcut) = parse_shortcut(s) {
let _ = app.global_shortcut().unregister(shortcut);
}
}
}
if let Ok(mut cur) = CURRENT_SHORTCUT.lock() {
*cur = None;
}
}
/// 创建弹窗窗口(隐藏状态)并注册失焦监听。
/// 位置默认在屏幕外,show_popup 时会重新定位到鼠标所在显示器中央。
/// 预创建后首次按快捷键走"窗口已存在"分支直接 show,避免首次创建的时序问题。
@@ -206,7 +158,7 @@ fn create_popup_window(app: &AppHandle) {
{
Ok(w) => w,
Err(e) => {
eprintln!("[quickpanel] 创建弹窗失败: {}", e);
crate::logger::log_error("quickpanel", &format!("创建弹窗失败: {}", e));
return;
}
};
@@ -217,11 +169,11 @@ fn create_popup_window(app: &AppHandle) {
win.on_window_event(move |event| {
if let tauri::WindowEvent::Focused(false) = event {
let _ = win_handle.hide();
let _ = app_handle.emit("quickpanel-hide", ());
let _ = app_handle.emit(crate::constants::events::QUICKPANEL_HIDE, ());
}
});
eprintln!("[quickpanel] 弹窗窗口已预创建(隐藏状态)");
crate::logger::log_info("quickpanel", "弹窗窗口已预创建(隐藏状态)");
}
/// 应用启动时预创建弹窗窗口(隐藏)。
@@ -250,41 +202,46 @@ pub fn show_popup(app: &AppHandle) {
let (wa_left, wa_top, wa_right, wa_bottom) = get_work_area_at_point(mx, my)
.unwrap_or((0, 0, 1920, 1040));
// 获取光标所在显示器的 DPI,将物理坐标转为逻辑坐标(DIP
// 光标所在显示器的 DPI:窗口尺寸需按物理像素换算
let dpi = get_dpi_for_point(mx, my).unwrap_or(96);
let scale = dpi as f64 / 96.0;
let win_w_px = WIN_W * scale;
let win_h_px = WIN_H * scale;
let wa_left_l = wa_left as f64 / scale;
let wa_top_l = wa_top as f64 / scale;
let wa_right_l = wa_right as f64 / scale;
let wa_bottom_l = wa_bottom as f64 / scale;
// 直接以物理坐标计算(光标 + 工作区均为物理像素,避免混合 DPI 下换算偏移)
let (x, y) = if cursor_mode {
// 鼠标位置模式:以鼠标为基准偏移,clamp 到工作区内
let mx_l = mx as f64 / scale;
let my_l = my as f64 / scale;
let x = (mx_l + 12.0).min(wa_right_l - WIN_W).max(wa_left_l);
let y = (my_l + 12.0).min(wa_bottom_l - WIN_H).max(wa_top_l);
let x = (mx as f64 + 12.0 * scale).min(wa_right as f64 - win_w_px).max(wa_left as f64);
let y = (my as f64 + 12.0 * scale).min(wa_bottom as f64 - win_h_px).max(wa_top as f64);
(x, y)
} else {
// 中央模式:窗口居中于鼠标所在显示器工作区
let wa_w = wa_right_l - wa_left_l;
let wa_h = wa_bottom_l - wa_top_l;
(wa_left_l + (wa_w - WIN_W) / 2.0, wa_top_l + (wa_h - WIN_H) / 2.0)
let wa_w = (wa_right - wa_left) as f64;
let wa_h = (wa_bottom - wa_top) as f64;
(
wa_left as f64 + (wa_w - win_w_px) / 2.0,
wa_top as f64 + (wa_h - win_h_px) / 2.0,
)
};
// 窗口已存在:移动 + 显示 + 请求焦点
if let Some(win) = app.get_webview_window(POPUP_LABEL) {
let _ = win.set_position(tauri::Position::Logical(tauri::LogicalPosition { x, y }));
let _ = win.set_position(tauri::Position::Physical(tauri::PhysicalPosition {
x: x as i32,
y: y as i32,
}));
let _ = win.show();
let _ = win.set_focus();
// 通知前端刷新数据
let _ = app.emit("quickpanel-show", ());
let _ = app.emit(crate::constants::events::QUICKPANEL_SHOW, ());
return;
}
// 兜底:窗口被销毁时重新创建(隐藏),等前端 onMounted 回调 show_window
POPUP_PENDING_SHOW.store(true, Ordering::SeqCst);
if let Ok(mut pos) = PENDING_POS.lock() {
*pos = Some((x, y));
}
create_popup_window(app);
}
@@ -296,10 +253,18 @@ pub fn show_window(app: &AppHandle) {
return;
}
if let Some(win) = app.get_webview_window(POPUP_LABEL) {
// 应用 show_popup 计算的兜底位置(物理坐标),避免停留在屏幕外
let pos = PENDING_POS.lock().ok().and_then(|p| *p);
if let Some((x, y)) = pos {
let _ = win.set_position(tauri::Position::Physical(tauri::PhysicalPosition {
x: x as i32,
y: y as i32,
}));
}
let _ = win.show();
let _ = win.set_focus();
// 通知前端刷新数据
let _ = app.emit("quickpanel-show", ());
let _ = app.emit(crate::constants::events::QUICKPANEL_SHOW, ());
}
}
@@ -9,8 +9,10 @@
use std::path::PathBuf;
use serde::Serialize;
use specta::Type;
/// 快捷位置条目
#[derive(Debug, Clone, Serialize)]
#[derive(Debug, Clone, Serialize, Type)]
#[serde(rename_all = "camelCase")]
pub struct SpecialLocation {
pub id: String,