优化,贴图

This commit is contained in:
zhongluofeng
2026-08-07 18:09:13 +08:00
parent e09b0567d6
commit d09599fa95
40 changed files with 1627 additions and 493 deletions
+3 -2
View File
@@ -1,8 +1,8 @@
{ {
"$schema": "../gen/schemas/desktop-schema.json", "$schema": "../gen/schemas/desktop-schema.json",
"identifier": "screenshot", "identifier": "screenshot",
"description": "Capability for screenshot overlay and editor windows", "description": "Capability for screenshot overlay, editor and pin windows",
"windows": ["screenshot-overlay*", "screenshot-editor-*"], "windows": ["screenshot-overlay*", "screenshot-editor-*", "screenshot-pin"],
"permissions": [ "permissions": [
"core:default", "core:default",
"core:window:allow-hide", "core:window:allow-hide",
@@ -11,6 +11,7 @@
"core:window:allow-start-dragging", "core:window:allow-start-dragging",
"core:window:allow-set-position", "core:window:allow-set-position",
"core:window:allow-set-size", "core:window:allow-set-size",
"core:window:allow-set-resizable",
"core:window:allow-set-always-on-top", "core:window:allow-set-always-on-top",
"core:window:allow-set-skip-taskbar", "core:window:allow-set-skip-taskbar",
"core:window:allow-set-decorations", "core:window:allow-set-decorations",
+3
View File
@@ -10,6 +10,8 @@ pub mod windows {
pub const OSD_OVERLAY: &str = "osd-overlay"; pub const OSD_OVERLAY: &str = "osd-overlay";
#[allow(dead_code)] #[allow(dead_code)]
pub const SCREENSHOT_OVERLAY: &str = "screenshot-overlay"; pub const SCREENSHOT_OVERLAY: &str = "screenshot-overlay";
#[allow(dead_code)]
pub const SCREENSHOT_PIN: &str = "screenshot-pin";
} }
/// Tauri 事件名(与前端 constants::EVENTS 对应) /// Tauri 事件名(与前端 constants::EVENTS 对应)
@@ -41,6 +43,7 @@ pub mod events {
pub const OSD_END_DRAG: &str = "osd-end-drag"; pub const OSD_END_DRAG: &str = "osd-end-drag";
// 截图 // 截图
pub const SCREENSHOT_SHORTCUT: &str = "screenshot-shortcut"; pub const SCREENSHOT_SHORTCUT: &str = "screenshot-shortcut";
pub const SCREENSHOT_PIN_SHORTCUT: &str = "screenshot-pin-shortcut";
// 内核安装进度 // 内核安装进度
pub const KERNEL_INSTALL_PROGRESS: &str = "kernel-install-progress"; pub const KERNEL_INSTALL_PROGRESS: &str = "kernel-install-progress";
// 进程与下载 // 进程与下载
+18 -12
View File
@@ -34,10 +34,10 @@ use mihomo_manager::{
proxy_test_delay, proxy_update_kernel, proxy_update_profile, proxy_version, MihomoManager, proxy_test_delay, proxy_update_kernel, proxy_update_profile, proxy_version, MihomoManager,
}; };
use monitor_kernel::{ use monitor_kernel::{
monitor_elevate_self, monitor_get_elevate_on_launch, monitor_get_hardware_config, monitor_elevate_self, monitor_get_auto_start, monitor_get_elevate_on_launch,
monitor_get_snapshot, monitor_get_status, monitor_kernel_info, monitor_set_elevate_on_launch, monitor_get_hardware_config, monitor_get_snapshot, monitor_get_status, monitor_kernel_info,
monitor_set_hardware_config, monitor_start, monitor_start_elevated, monitor_status, monitor_set_auto_start, monitor_set_elevate_on_launch, monitor_set_hardware_config,
monitor_stop, MonitorKernel, monitor_start, monitor_start_elevated, monitor_status, monitor_stop, MonitorKernel,
}; };
use network_monitor::network_status; use network_monitor::network_status;
use osd_window::{ use osd_window::{
@@ -54,8 +54,9 @@ use screenshot::commands::{
screenshot_crop_stored, screenshot_cursor_pos, screenshot_delete_cache, screenshot_crop_stored, screenshot_cursor_pos, screenshot_delete_cache,
screenshot_disable_transitions, screenshot_enum_windows, screenshot_fullscreen_png, screenshot_disable_transitions, screenshot_enum_windows, screenshot_fullscreen_png,
screenshot_get_editor_image, screenshot_get_fullscreen_bmp, screenshot_load_cache, screenshot_get_editor_image, screenshot_get_fullscreen_bmp, screenshot_load_cache,
screenshot_register_shortcut, screenshot_save_cache, screenshot_save_png, screenshot_register_pin_shortcut, screenshot_register_shortcut, screenshot_save_cache,
screenshot_set_editor_image, screenshot_unregister_shortcut, screenshot_window_from_point, screenshot_save_png, screenshot_set_editor_image, screenshot_unregister_pin_shortcut,
screenshot_unregister_shortcut, screenshot_window_from_point,
}; };
use clipboard::{ use clipboard::{
ClipboardManager, ClipboardManager,
@@ -127,13 +128,14 @@ fn export_bindings() {
downloader_get_tasks, downloader_check_url, downloader_add_task, downloader_pause_task, downloader_get_tasks, downloader_check_url, downloader_add_task, downloader_pause_task,
downloader_resume_task, downloader_remove_task, downloader_get_settings, downloader_resume_task, downloader_remove_task, downloader_get_settings,
downloader_save_settings, downloader_open_dir, downloader_open_url, downloader_save_settings, downloader_open_dir, downloader_open_url,
// screenshot19,豁免 2get_fullscreen_bmp 返回 ipc::Response、compose_copy 接收 ipc::Request // screenshot21,豁免 2get_fullscreen_bmp 返回 ipc::Response、compose_copy 接收 ipc::Request
screenshot_disable_transitions, screenshot_register_shortcut, screenshot_disable_transitions, screenshot_register_shortcut,
screenshot_unregister_shortcut, screenshot_capture_fullscreen, screenshot_fullscreen_png, screenshot_unregister_shortcut, screenshot_register_pin_shortcut,
screenshot_clear_fullscreen, screenshot_crop_stored, screenshot_crop_copy_stored, screenshot_unregister_pin_shortcut, screenshot_capture_fullscreen,
screenshot_window_from_point, screenshot_cursor_pos, screenshot_enum_windows, screenshot_fullscreen_png, screenshot_clear_fullscreen, screenshot_crop_stored,
screenshot_capture_window, screenshot_set_editor_image, screenshot_get_editor_image, screenshot_crop_copy_stored, screenshot_window_from_point, screenshot_cursor_pos,
screenshot_copy_image, screenshot_save_png, screenshot_enum_windows, screenshot_capture_window, screenshot_set_editor_image,
screenshot_get_editor_image, screenshot_copy_image, screenshot_save_png,
screenshot_save_cache, screenshot_load_cache, screenshot_delete_cache, screenshot_save_cache, screenshot_load_cache, screenshot_delete_cache,
]) ])
.export(Typescript::default(), "../src/lib/bindings.ts") .export(Typescript::default(), "../src/lib/bindings.ts")
@@ -204,6 +206,8 @@ pub fn run() {
monitor_get_snapshot, monitor_get_snapshot,
monitor_get_elevate_on_launch, monitor_get_elevate_on_launch,
monitor_set_elevate_on_launch, monitor_set_elevate_on_launch,
monitor_get_auto_start,
monitor_set_auto_start,
monitor_get_hardware_config, monitor_get_hardware_config,
monitor_set_hardware_config, monitor_set_hardware_config,
network_status, network_status,
@@ -291,6 +295,8 @@ pub fn run() {
screenshot_delete_cache, screenshot_delete_cache,
screenshot_register_shortcut, screenshot_register_shortcut,
screenshot_unregister_shortcut, screenshot_unregister_shortcut,
screenshot_register_pin_shortcut,
screenshot_unregister_pin_shortcut,
screenshot_disable_transitions, screenshot_disable_transitions,
screenshot_compose_copy screenshot_compose_copy
]) ])
+1
View File
@@ -14,6 +14,7 @@ mod system_proxy;
mod types; mod types;
pub use pseudo::is_pseudo_node; pub use pseudo::is_pseudo_node;
pub use system_proxy::get_system_proxy_windows;
pub use types::{InstallProgress, KernelInfo, KernelUpdateInfo, ProfileMeta, ProxySettings, ProxyStatus}; pub use types::{InstallProgress, KernelInfo, KernelUpdateInfo, ProfileMeta, ProxySettings, ProxyStatus};
pub use commands::{ pub use commands::{
proxy_activate_profile, proxy_check_kernel_update, proxy_clear_system_proxy, proxy_close_connection, proxy_activate_profile, proxy_check_kernel_update, proxy_clear_system_proxy, proxy_close_connection,
+2 -2
View File
@@ -38,7 +38,7 @@ pub(crate) fn clear_system_proxy_windows() -> Result<(), String> {
} }
#[cfg(windows)] #[cfg(windows)]
pub(crate) fn get_system_proxy_windows() -> bool { pub fn get_system_proxy_windows() -> bool {
use winreg::enums::*; use winreg::enums::*;
use winreg::RegKey; use winreg::RegKey;
let hkcu = RegKey::predef(HKEY_CURRENT_USER); let hkcu = RegKey::predef(HKEY_CURRENT_USER);
@@ -67,7 +67,7 @@ pub(crate) fn clear_system_proxy_windows() -> Result<(), String> {
Err("系统代理仅支持 Windows".into()) Err("系统代理仅支持 Windows".into())
} }
#[cfg(not(windows))] #[cfg(not(windows))]
pub(crate) fn get_system_proxy_windows() -> bool { pub fn get_system_proxy_windows() -> bool {
false false
} }
+80
View File
@@ -132,6 +132,41 @@ pub fn is_thing_elevated() -> bool {
false false
} }
// ===================== 监控设置持久化 =====================
/// 监控模块设置(存储于 {app_data_dir}/monitor/settings.json
/// 参照代理模块 ProxySettings 的模式,仅包含需要持久化的运行时开关。
#[derive(Serialize, Deserialize, Clone, Default)]
#[serde(rename_all = "camelCase")]
pub struct MonitorSettings {
/// 应用启动时自动启动监控内核(默认 false)
#[serde(default)]
pub auto_start: bool,
}
/// 监控设置文件路径: {app_data_dir}/monitor/settings.json
fn settings_path(app_data_dir: &std::path::Path) -> PathBuf {
app_data_dir.join("monitor").join("settings.json")
}
/// 读取监控设置(文件不存在或解析失败时返回默认值)
pub fn load_monitor_settings(app_data_dir: &std::path::Path) -> MonitorSettings {
fs::read_to_string(settings_path(app_data_dir))
.ok()
.and_then(|s| serde_json::from_str::<MonitorSettings>(&s).ok())
.unwrap_or_default()
}
/// 写入监控设置
pub fn save_monitor_settings(app_data_dir: &std::path::Path, settings: &MonitorSettings) -> Result<(), String> {
let path = settings_path(app_data_dir);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|e| format!("创建目录失败: {}", e))?;
}
let content = serde_json::to_string_pretty(settings).map_err(|e| format!("序列化设置失败: {}", e))?;
fs::write(path, content).map_err(|e| format!("写入设置失败: {}", e))
}
// ===================== 永久提权标志持久化 ===================== // ===================== 永久提权标志持久化 =====================
/// 永久提权标志文件路径: {app_data_dir}/monitor/elevate.json /// 永久提权标志文件路径: {app_data_dir}/monitor/elevate.json
@@ -295,6 +330,32 @@ impl MonitorKernel {
write_elevate_flag(&self.root.join("elevate.json"), enabled) write_elevate_flag(&self.root.join("elevate.json"), enabled)
} }
/// 读取监控设置(auto_start 等)
pub fn load_settings(&self) -> MonitorSettings {
load_monitor_settings(&self.root.parent().unwrap_or(&self.root).to_path_buf())
}
/// 写入监控设置
pub fn save_settings(&self, settings: &MonitorSettings) -> Result<(), String> {
save_monitor_settings(&self.root.parent().unwrap_or(&self.root).to_path_buf(), settings)
}
/// 应用启动时检查是否需要自动启动监控内核
pub fn auto_start_on_launch(&self, app: &AppHandle) {
let settings = self.load_settings();
if !settings.auto_start {
return;
}
let monitor = self.clone();
let app_handle = app.clone();
tauri::async_runtime::spawn(async move {
match monitor.start_with_subscription(&app_handle).await {
Ok(info) => crate::logger::log_info("monitor", &format!("自动启动成功, pid={:?}", info.pid)),
Err(e) => crate::logger::log_warn("monitor", &format!("自动启动跳过: {}", e)),
}
});
}
fn cores_dir(&self) -> PathBuf { fn cores_dir(&self) -> PathBuf {
self.root.join("cores") self.root.join("cores")
} }
@@ -950,6 +1011,25 @@ pub fn monitor_set_elevate_on_launch(
state.set_elevate_on_launch(enabled) state.set_elevate_on_launch(enabled)
} }
/// 查询"应用启动时自动启动监控内核"是否已启用
#[tauri::command]
pub fn monitor_get_auto_start(
state: tauri::State<'_, MonitorKernel>,
) -> bool {
state.load_settings().auto_start
}
/// 设置/清除"应用启动时自动启动监控内核"开关
#[tauri::command]
pub fn monitor_set_auto_start(
state: tauri::State<'_, MonitorKernel>,
enabled: bool,
) -> Result<(), String> {
let mut settings = state.load_settings();
settings.auto_start = enabled;
state.save_settings(&settings)
}
/// 查询硬件监控配置(透传 Kernel GET /config/hardware)。 /// 查询硬件监控配置(透传 Kernel GET /config/hardware)。
/// 返回当前配置 + 可用硬件/传感器类型清单,供前端 Dialog 渲染。 /// 返回当前配置 + 可用硬件/传感器类型清单,供前端 Dialog 渲染。
#[tauri::command] #[tauri::command]
+8 -1
View File
@@ -114,6 +114,8 @@ pub async fn quickpanel_build_file_index(app: AppHandle) -> Result<i64, String>
} else { } else {
settings.index_dirs settings.index_dirs
}; };
// 懒加载:首次构建时自动初始化 DB 连接
file_index::ensure_initialized(&app);
// 阻塞操作放到 spawn_blocking // 阻塞操作放到 spawn_blocking
tauri::async_runtime::spawn_blocking(move || file_index::build_index(&dirs)) tauri::async_runtime::spawn_blocking(move || file_index::build_index(&dirs))
.await .await
@@ -126,7 +128,10 @@ pub async fn quickpanel_build_file_index(app: AppHandle) -> Result<i64, String>
pub async fn quickpanel_search_files( pub async fn quickpanel_search_files(
query: String, query: String,
limit: Option<i64>, limit: Option<i64>,
app: AppHandle,
) -> Result<Vec<file_index::FileRecord>, String> { ) -> Result<Vec<file_index::FileRecord>, String> {
// 懒加载:首次搜索时自动初始化 DB 连接
file_index::ensure_initialized(&app);
tauri::async_runtime::spawn_blocking(move || file_index::search(&query, limit.unwrap_or(50))) tauri::async_runtime::spawn_blocking(move || file_index::search(&query, limit.unwrap_or(50)))
.await .await
.map_err(|e| format!("搜索任务失败: {}", e)) .map_err(|e| format!("搜索任务失败: {}", e))
@@ -135,7 +140,9 @@ pub async fn quickpanel_search_files(
/// 获取索引状态 /// 获取索引状态
#[tauri::command] #[tauri::command]
#[specta::specta] #[specta::specta]
pub fn quickpanel_file_index_stats() -> file_index::IndexStats { pub async fn quickpanel_file_index_stats(app: AppHandle) -> file_index::IndexStats {
// 懒加载:查询状态前确保 DB 已初始化(未初始化时 stats 返回全 0)
file_index::ensure_initialized(&app);
file_index::stats() file_index::stats()
} }
+11
View File
@@ -93,6 +93,17 @@ pub fn init(app: &AppHandle) {
crate::logger::log_info("quickpanel", &format!("文件索引 DB 已就绪: {}", path.display())); crate::logger::log_info("quickpanel", &format!("文件索引 DB 已就绪: {}", path.display()));
} }
/// 懒加载:首次访问文件索引时自动初始化(若尚未初始化)。
/// 避免应用启动时即打开 SQLite 连接,降低启动 IO 开销。
pub fn ensure_initialized(app: &AppHandle) {
let guard = index_slot().lock().unwrap_or_else(|e| e.into_inner());
if guard.is_some() {
return;
}
drop(guard);
init(app);
}
/// 判断索引是否已初始化 /// 判断索引是否已初始化
fn with_conn<F, R>(f: F) -> Option<R> fn with_conn<F, R>(f: F) -> Option<R>
where where
+26 -5
View File
@@ -67,6 +67,27 @@ pub async fn screenshot_unregister_shortcut(app: tauri::AppHandle) -> Result<(),
Ok(()) Ok(())
} }
/// 注册(或切换)贴图全局快捷键。传入空字符串则禁用快捷键。
/// 按下时 emit 'screenshot-pin-shortcut',由前端切换贴图窗口显示/隐藏。
#[tauri::command]
#[specta::specta]
pub async fn screenshot_register_pin_shortcut(
app: tauri::AppHandle,
shortcut: String,
) -> Result<(), String> {
crate::shortcut::register_shortcut(&app, "贴图", &shortcut, |a| {
let _ = a.emit(crate::constants::events::SCREENSHOT_PIN_SHORTCUT, ());
})
}
/// 注销贴图全局快捷键
#[tauri::command]
#[specta::specta]
pub async fn screenshot_unregister_pin_shortcut(app: tauri::AppHandle) -> Result<(), String> {
crate::shortcut::unregister_shortcut(&app, "贴图");
Ok(())
}
/// 捕获整个虚拟屏(多显示器拼接)存入静态,不做 PNG 编码 /// 捕获整个虚拟屏(多显示器拼接)存入静态,不做 PNG 编码
#[tauri::command] #[tauri::command]
#[specta::specta] #[specta::specta]
@@ -349,17 +370,17 @@ pub async fn screenshot_save_png(png_base64: String, path: String) -> Result<(),
.map_err(|e| format!("保存任务失败: {}", e))? .map_err(|e| format!("保存任务失败: {}", e))?
} }
// ===== 截图历史缓存:完整 PNG 落盘缓存目录,内存只保留缩略图 ===== // ===== 截图历史缓存:完整 PNG 落盘到应用数据目录(持久化,随历史保留数量清理),内存只保留缩略图 =====
/// 历史缓存根目录(app_cache_dir/screenshot/history /// 历史根目录(app_data_dir/screenshot/history
fn history_cache_dir(app: &tauri::AppHandle) -> Result<std::path::PathBuf, String> { fn history_cache_dir(app: &tauri::AppHandle) -> Result<std::path::PathBuf, String> {
let dir = app let dir = app
.path() .path()
.app_cache_dir() .app_data_dir()
.map_err(|e| format!("获取缓存目录失败: {}", e))? .map_err(|e| format!("获取应用数据目录失败: {}", e))?
.join("screenshot") .join("screenshot")
.join("history"); .join("history");
std::fs::create_dir_all(&dir).map_err(|e| format!("创建缓存目录失败: {}", e))?; std::fs::create_dir_all(&dir).map_err(|e| format!("创建历史目录失败: {}", e))?;
Ok(dir) Ok(dir)
} }
+5 -12
View File
@@ -90,8 +90,10 @@ pub fn init(app: &mut App<Wry>) -> Result<(), Box<dyn std::error::Error>> {
} }
app.manage(clipboard); app.manage(clipboard);
// ===== 快速面板:快捷键 + 预创建弹窗 + 文件索引 ===== // ===== 快速面板:快捷键 + 预创建弹窗 =====
// defaultEnabled:true 假设启用;用户在设置页禁用模块时由前端 onDisable 钩子注销快捷键。 // defaultEnabled:true 假设启用;用户在设置页禁用模块时由前端 onDisable 钩子注销快捷键。
// 文件索引 DB 连接改为懒加载(首次搜索/构建时由 commands 中的 ensure_initialized 触发),
// 避免应用启动时即打开 SQLite 连接,降低启动 IO 开销。
let qp_settings = crate::quickpanel::load_settings(&app.handle()); let qp_settings = crate::quickpanel::load_settings(&app.handle());
if !qp_settings.shortcut.trim().is_empty() { if !qp_settings.shortcut.trim().is_empty() {
let app_handle = app.handle().clone(); let app_handle = app.handle().clone();
@@ -106,8 +108,6 @@ pub fn init(app: &mut App<Wry>) -> Result<(), Box<dyn std::error::Error>> {
// 预创建弹窗窗口(隐藏),首次按快捷键时直接 show,避免首次创建时序问题 // 预创建弹窗窗口(隐藏),首次按快捷键时直接 show,避免首次创建时序问题
crate::quickpanel::ensure_window(&app_handle); crate::quickpanel::ensure_window(&app_handle);
} }
// 初始化文件索引数据库(不立即构建,由前端设置页或首次唤起时触发)
crate::quickpanel::file_index::init(&app.handle());
// ===== 托盘菜单 ===== // ===== 托盘菜单 =====
crate::tray_menu::create_tray_menu(app.handle())?; crate::tray_menu::create_tray_menu(app.handle())?;
@@ -123,16 +123,9 @@ pub fn init(app: &mut App<Wry>) -> Result<(), Box<dyn std::error::Error>> {
} }
} }
// monitor Kernel硬件监控默认启用,被动读取无副作用 // monitor Kernel用户在设置中开启"自动启动"时随应用启动
if let Some(monitor) = app.try_state::<MonitorKernel>() { if let Some(monitor) = app.try_state::<MonitorKernel>() {
let monitor = monitor.inner().clone(); monitor.auto_start_on_launch(app.handle());
let app_handle = app.handle().clone();
tauri::async_runtime::spawn(async move {
match monitor.start_with_subscription(&app_handle).await {
Ok(info) => crate::logger::log_info("monitor", &format!("自动启动成功, pid={:?}", info.pid)),
Err(e) => crate::logger::log_warn("monitor", &format!("自动启动跳过: {}", e)),
}
});
} }
// 截图快捷键由前端 screenshotStore 启动时调用 screenshot_register_shortcut 注册 // 截图快捷键由前端 screenshotStore 启动时调用 screenshot_register_shortcut 注册
+19
View File
@@ -46,6 +46,7 @@ pub struct ProxyNodeInfo {
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct TrayMenuState { pub struct TrayMenuState {
pub proxy_running: bool, pub proxy_running: bool,
pub system_proxy: bool,
pub monitor_running: bool, pub monitor_running: bool,
pub proxy_group: Option<String>, pub proxy_group: Option<String>,
pub proxy_nodes: Vec<ProxyNodeInfo>, pub proxy_nodes: Vec<ProxyNodeInfo>,
@@ -150,6 +151,8 @@ async fn fetch_proxy_nodes(app: &AppHandle) -> Option<(String, Vec<(String, Opti
pub async fn get_tray_menu_state(app: &AppHandle) -> TrayMenuState { pub async fn get_tray_menu_state(app: &AppHandle) -> TrayMenuState {
let proxy_running = is_proxy_running(app); let proxy_running = is_proxy_running(app);
let monitor_running = is_monitor_running(app); let monitor_running = is_monitor_running(app);
// 读取 Windows 注册表中的真实系统代理状态(不依赖 settings.json 缓存)
let system_proxy = crate::mihomo_manager::get_system_proxy_windows();
let (proxy_group, proxy_nodes, proxy_current) = if proxy_running { let (proxy_group, proxy_nodes, proxy_current) = if proxy_running {
match fetch_proxy_nodes(app).await { match fetch_proxy_nodes(app).await {
@@ -172,6 +175,7 @@ pub async fn get_tray_menu_state(app: &AppHandle) -> TrayMenuState {
TrayMenuState { TrayMenuState {
proxy_running, proxy_running,
system_proxy,
monitor_running, monitor_running,
proxy_group, proxy_group,
proxy_nodes, proxy_nodes,
@@ -338,6 +342,21 @@ pub async fn tray_menu_action(
} }
} }
} }
"system_proxy_toggle" => {
// 根据注册表当前真实状态切换(不依赖 settings.json 缓存,避免与主界面不同步)
let mihomo = app.state::<MihomoManager>();
if crate::mihomo_manager::get_system_proxy_windows() {
if let Err(e) = mihomo.disable_system_proxy() {
crate::logger::log_error("tray", &format!("关闭系统代理失败: {}", e));
send_notification(&app, "系统代理关闭失败", &e);
}
} else {
if let Err(e) = mihomo.enable_system_proxy() {
crate::logger::log_error("tray", &format!("开启系统代理失败: {}", e));
send_notification(&app, "系统代理开启失败", &e);
}
}
}
"osd_toggle" => { "osd_toggle" => {
let _ = app.emit(crate::constants::events::TRAY_TOGGLE_OSD, ()); let _ = app.emit(crate::constants::events::TRAY_TOGGLE_OSD, ());
} }
+6 -1
View File
@@ -37,6 +37,11 @@
"icons/icon.icns", "icons/icon.icns",
"icons/icon.ico" "icons/icon.ico"
], ],
"resources": ["binaries/*", "resources/thing-extension/**/*"] "resources": ["binaries/*", "resources/thing-extension/**/*"],
"windows": {
"nsis": {
"compression": "lzma"
}
}
} }
} }
+4
View File
@@ -10,6 +10,7 @@ import { useAppStore } from '@/stores/appStore'
import { useScreenshotStore } from '@/stores/screenshotStore' import { useScreenshotStore } from '@/stores/screenshotStore'
import { useQuickPanelStore } from '@/stores/quickpanelStore' import { useQuickPanelStore } from '@/stores/quickpanelStore'
import { useMonitorStore } from '@/stores/monitorStore' import { useMonitorStore } from '@/stores/monitorStore'
import { useProcessStore } from '@/stores/processStore'
import { TooltipProvider } from '@/components/ui/tooltip' import { TooltipProvider } from '@/components/ui/tooltip'
import { moduleRegistry } from '@/modules/registry' import { moduleRegistry } from '@/modules/registry'
import type { ModuleMeta } from '@/types/module' import type { ModuleMeta } from '@/types/module'
@@ -20,6 +21,7 @@ const appStore = useAppStore()
const screenshotStore = useScreenshotStore() const screenshotStore = useScreenshotStore()
const quickpanelStore = useQuickPanelStore() const quickpanelStore = useQuickPanelStore()
const monitorStore = useMonitorStore() const monitorStore = useMonitorStore()
const processStore = useProcessStore()
/** 侧边栏 / 标题栏需要的模块信息(id + name + icon */ /** 侧边栏 / 标题栏需要的模块信息(id + name + icon */
interface NavModule { interface NavModule {
@@ -193,6 +195,8 @@ onUnmounted(() => {
screenshotStore.destroyExportListener() screenshotStore.destroyExportListener()
// 释放 OSD 事件监听与配置 watcher // 释放 OSD 事件监听与配置 watcher
monitorStore.disposeOsd() monitorStore.disposeOsd()
// 释放进程状态事件监听
processStore.destroyListener()
}) })
</script> </script>
+4 -4
View File
@@ -31,14 +31,14 @@ const displayModules = computed(() => {
<Button <Button
:variant="activeModule === module.id ? 'default' : 'ghost'" :variant="activeModule === module.id ? 'default' : 'ghost'"
size="icon" size="icon"
class="h-10 w-10 rounded-lg transition-[background-color,color,box-shadow] duration-300" class="size-10 rounded-lg transition-[background-color,color,box-shadow] duration-300"
:class="{ :class="{
'bg-primary text-primary-foreground shadow-md': activeModule === module.id, 'bg-primary text-primary-foreground shadow-md': activeModule === module.id,
'hover:bg-secondary/50': activeModule !== module.id 'hover:bg-secondary/50': activeModule !== module.id
}" }"
@click="emit('change', module.id)" @click="emit('change', module.id)"
> >
<component :is="getModuleIcon(module.icon)" class="h-5 w-5" /> <component :is="getModuleIcon(module.icon)" class="size-4" />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="right" class="w-fit"> <TooltipContent side="right" class="w-fit">
@@ -55,14 +55,14 @@ const displayModules = computed(() => {
<Button <Button
:variant="activeModule === 'settings' ? 'default' : 'ghost'" :variant="activeModule === 'settings' ? 'default' : 'ghost'"
size="icon" size="icon"
class="h-10 w-10 rounded-lg transition-[background-color,color,box-shadow] duration-300" class="size-10 rounded-lg transition-[background-color,color,box-shadow] duration-300"
:class="{ :class="{
'bg-primary text-primary-foreground shadow-md': activeModule === 'settings', 'bg-primary text-primary-foreground shadow-md': activeModule === 'settings',
'hover:bg-secondary/50': activeModule !== 'settings' 'hover:bg-secondary/50': activeModule !== 'settings'
}" }"
@click="emit('change', 'settings')" @click="emit('change', 'settings')"
> >
<Settings class="h-5 w-5" /> <Settings class="size-4" />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="right" class="w-fit"> <TooltipContent side="right" class="w-fit">
+18 -11
View File
@@ -2,6 +2,7 @@
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue' import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
import { Search, Settings, ChevronRight, ArrowUp, Check, Loader2 } from '@lucide/vue' import { Search, Settings, ChevronRight, ArrowUp, Check, Loader2 } from '@lucide/vue'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'
import { invoke } from '@tauri-apps/api/core' import { invoke } from '@tauri-apps/api/core'
import { getCurrentWindow } from '@tauri-apps/api/window' import { getCurrentWindow } from '@tauri-apps/api/window'
import { useSearchStore, type SearchItem } from '@/stores/searchStore' import { useSearchStore, type SearchItem } from '@/stores/searchStore'
@@ -257,26 +258,29 @@ const handleBlur = () => {
</div> </div>
</Transition> </Transition>
<Transition name="floating-tabs"> <Transition name="floating-tabs">
<Tooltip v-if="showScrollTop">
<TooltipTrigger as-child>
<button <button
v-if="showScrollTop" class="flex items-center justify-center size-6 rounded-md text-muted-foreground hover:text-foreground hover:bg-secondary/60 transition-colors"
class="flex items-center justify-center h-6 w-6 rounded-md text-muted-foreground hover:text-foreground hover:bg-secondary/60 transition-colors"
title="回到顶部"
@click="scrollToTop" @click="scrollToTop"
@mousedown.stop @mousedown.stop
> >
<ArrowUp class="h-3.5 w-3.5" /> <ArrowUp class="h-3.5 w-3.5" />
</button> </button>
</TooltipTrigger>
<TooltipContent>回到顶部</TooltipContent>
</Tooltip>
</Transition> </Transition>
</div> </div>
<div class="flex-1"></div> <div class="flex-1"></div>
<!-- 保存设置按钮当前模块在设置 tab 且注册了保存函数时显示从右向左弹出 --> <!-- 保存设置按钮当前模块在设置 tab 且注册了保存函数时显示从右向左弹出 -->
<Transition name="save-btn"> <Transition name="save-btn">
<Tooltip v-if="tabsStore.saveVisible">
<TooltipTrigger as-child>
<button <button
v-if="tabsStore.saveVisible"
class="flex items-center gap-1.5 h-7 px-3 mr-1 text-xs font-medium rounded-md bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-60 disabled:pointer-events-none transition-colors pointer-events-auto" class="flex items-center gap-1.5 h-7 px-3 mr-1 text-xs font-medium rounded-md bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-60 disabled:pointer-events-none transition-colors pointer-events-auto"
:disabled="tabsStore.saving" :disabled="tabsStore.saving"
title="保存当前模块设置"
@click="tabsStore.runSave" @click="tabsStore.runSave"
@mousedown.stop @mousedown.stop
> >
@@ -284,10 +288,13 @@ const handleBlur = () => {
<Check v-else class="size-3.5" /> <Check v-else class="size-3.5" />
保存设置 保存设置
</button> </button>
</TooltipTrigger>
<TooltipContent>保存当前模块设置</TooltipContent>
</Tooltip>
</Transition> </Transition>
<div class="relative max-w-xs mr-3 pointer-events-auto"> <div class="relative max-w-xs mr-3 pointer-events-auto">
<Search <Search
class="absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" class="absolute left-2.5 top-1/2 -translate-y-1/2 size-4 text-muted-foreground"
/> />
<Input <Input
v-model="searchQuery" v-model="searchQuery"
@@ -325,14 +332,14 @@ const handleBlur = () => {
class="w-full px-3 py-2 text-left text-sm hover:bg-accent transition-colors flex items-center gap-2" class="w-full px-3 py-2 text-left text-sm hover:bg-accent transition-colors flex items-center gap-2"
@click="handleSettingSelect(item)" @click="handleSettingSelect(item)"
> >
<Settings class="h-4 w-4 text-muted-foreground" /> <Settings class="size-4 text-muted-foreground" />
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
<span class="truncate">{{ item.title }}</span> <span class="truncate">{{ item.title }}</span>
<span v-if="item.description" class="block text-xs text-muted-foreground truncate"> <span v-if="item.description" class="block text-xs text-muted-foreground truncate">
{{ item.description }} {{ item.description }}
</span> </span>
</div> </div>
<ChevronRight class="h-4 w-4 text-muted-foreground" /> <ChevronRight class="size-4 text-muted-foreground" />
</button> </button>
</template> </template>
@@ -347,7 +354,7 @@ const handleBlur = () => {
<div class="flex items-center pointer-events-auto" :class="{ 'hover-suppressed': hoverSuppressed }"> <div class="flex items-center pointer-events-auto" :class="{ 'hover-suppressed': hoverSuppressed }">
<button <button
class="h-10 w-10 flex items-center justify-center hover:bg-foreground/5 transition-colors rounded-sm" class="size-10 flex items-center justify-center hover:bg-foreground/5 transition-colors rounded-sm"
@click="minimize" @click="minimize"
@mousedown.stop @mousedown.stop
> >
@@ -358,7 +365,7 @@ const handleBlur = () => {
</button> </button>
<button <button
id="titlebar-maximize" id="titlebar-maximize"
class="h-10 w-10 flex items-center justify-center transition-colors rounded-sm titlebar-maximize-btn" class="size-10 flex items-center justify-center transition-colors rounded-sm titlebar-maximize-btn"
@click="maximize" @click="maximize"
@mousedown.stop @mousedown.stop
> >
@@ -375,7 +382,7 @@ const handleBlur = () => {
</svg> </svg>
</button> </button>
<button <button
class="h-10 w-10 flex items-center justify-center hover:bg-red-500 hover:text-white text-foreground transition-colors rounded-sm" class="size-10 flex items-center justify-center hover:bg-red-500 hover:text-white text-foreground transition-colors rounded-sm"
@click="close" @click="close"
@mousedown.stop @mousedown.stop
> >
+1 -1
View File
@@ -24,7 +24,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
data-slot="switch" data-slot="switch"
v-bind="forwarded" v-bind="forwarded"
:class="cn( :class="cn(
'peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-3 disabled:cursor-not-allowed disabled:opacity-50', 'peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-3 disabled:cursor-not-allowed disabled:opacity-50 cursor-pointer hover:data-[state=checked]:bg-primary/90 hover:data-[state=unchecked]:bg-input/70 active:scale-95',
props.class, props.class,
)" )"
> >
+1 -1
View File
@@ -24,7 +24,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
<TooltipContent <TooltipContent
data-slot="tooltip-content" data-slot="tooltip-content"
v-bind="{ ...forwarded, ...$attrs }" v-bind="{ ...forwarded, ...$attrs }"
:class="cn('bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit rounded-md px-3 py-1.5 text-xs text-balance', props.class)" :class="cn('bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit max-w-[calc(100vw-2rem)] rounded-md px-3 py-1.5 text-xs', props.class)"
> >
<slot /> <slot />
+7
View File
@@ -140,6 +140,13 @@ export const commands = {
screenshotRegisterShortcut: (shortcut: string) => __TAURI_INVOKE<null>("screenshot_register_shortcut", { shortcut }), screenshotRegisterShortcut: (shortcut: string) => __TAURI_INVOKE<null>("screenshot_register_shortcut", { shortcut }),
/** 注销截图全局快捷键 */ /** 注销截图全局快捷键 */
screenshotUnregisterShortcut: () => __TAURI_INVOKE<null>("screenshot_unregister_shortcut"), screenshotUnregisterShortcut: () => __TAURI_INVOKE<null>("screenshot_unregister_shortcut"),
/**
* 注册(或切换)贴图全局快捷键。传入空字符串则禁用快捷键。
* 按下时 emit 'screenshot-pin-shortcut',由前端切换贴图窗口显示/隐藏。
*/
screenshotRegisterPinShortcut: (shortcut: string) => __TAURI_INVOKE<null>("screenshot_register_pin_shortcut", { shortcut }),
/** 注销贴图全局快捷键 */
screenshotUnregisterPinShortcut: () => __TAURI_INVOKE<null>("screenshot_unregister_pin_shortcut"),
/** 捕获整个虚拟屏(多显示器拼接)存入静态,不做 PNG 编码 */ /** 捕获整个虚拟屏(多显示器拼接)存入静态,不做 PNG 编码 */
screenshotCaptureFullscreen: () => __TAURI_INVOKE<null>("screenshot_capture_fullscreen"), screenshotCaptureFullscreen: () => __TAURI_INVOKE<null>("screenshot_capture_fullscreen"),
/** 全屏捕获编码为 PNG base64 并清除(全屏截图直接进编辑器) */ /** 全屏捕获编码为 PNG base64 并清除(全屏截图直接进编辑器) */
+6
View File
@@ -9,6 +9,7 @@ export const WINDOWS = {
main: 'main', main: 'main',
osdOverlay: 'osd-overlay', osdOverlay: 'osd-overlay',
screenshotOverlay: 'screenshot-overlay', screenshotOverlay: 'screenshot-overlay',
screenshotPin: 'screenshot-pin',
} as const } as const
/** Tauri 事件名(前端 emit / listen 与 Rust constants::events 对应) */ /** Tauri 事件名(前端 emit / listen 与 Rust constants::events 对应) */
@@ -31,6 +32,9 @@ export const EVENTS = {
screenshotBegin: 'screenshot-begin', screenshotBegin: 'screenshot-begin',
screenshotOverlayReady: 'screenshot-overlay-ready', screenshotOverlayReady: 'screenshot-overlay-ready',
screenshotShortcut: 'screenshot-shortcut', screenshotShortcut: 'screenshot-shortcut',
screenshotPinShortcut: 'screenshot-pin-shortcut',
screenshotPinReady: 'screenshot-pin-ready',
screenshotPinShow: 'screenshot-pin-show',
screenshotExported: 'screenshot-exported', screenshotExported: 'screenshot-exported',
// 内核安装进度 // 内核安装进度
kernelInstallProgress: 'kernel-install-progress', kernelInstallProgress: 'kernel-install-progress',
@@ -62,4 +66,6 @@ export const STORAGE_KEYS = {
quickpanelHistoryItems: 'thing_quickpanel_history_items', quickpanelHistoryItems: 'thing_quickpanel_history_items',
currencyRates: 'thing_quickpanel_currency_rates', currencyRates: 'thing_quickpanel_currency_rates',
monitorOsdConfig: 'thing_monitor_osd_config', monitorOsdConfig: 'thing_monitor_osd_config',
screenshotHistory: 'thing_screenshot_history',
screenshotPinIndex: 'thing_screenshot_pin_index',
} as const } as const
+5 -1
View File
@@ -26,6 +26,7 @@ const standaloneWindowApps: Array<[hash: string, label: string, loader: () => Pr
['#tray-menu', '托盘菜单', () => import('./modules/tray/TrayMenu.vue')], ['#tray-menu', '托盘菜单', () => import('./modules/tray/TrayMenu.vue')],
['#screenshot-overlay', '截图覆盖层', () => import('./modules/screenshot/ScreenshotOverlay.vue')], ['#screenshot-overlay', '截图覆盖层', () => import('./modules/screenshot/ScreenshotOverlay.vue')],
['#screenshot-editor', '截图编辑器', () => import('./modules/screenshot/ScreenshotEditor.vue')], ['#screenshot-editor', '截图编辑器', () => import('./modules/screenshot/ScreenshotEditor.vue')],
['#screenshot-pin', '贴图窗口', () => import('./modules/screenshot/ScreenshotPin.vue')],
] ]
const winHash = window.location.hash const winHash = window.location.hash
@@ -77,14 +78,17 @@ if (matched) {
useProcessStore().initListener().catch(e => console.error('Process listener init error:', e)) useProcessStore().initListener().catch(e => console.error('Process listener init error:', e))
}) })
// 截图模块:加载设置 + 预创建常驻覆盖层窗口 + 监听导出事件(历史/自动保存)+ 注册并监听全局快捷键 // 截图模块:加载设置 + 恢复持久化历史 + 预创建常驻覆盖层窗口 + 监听导出事件(历史/自动保存)+ 注册并监听全局快捷键(截图/贴图)
void import('./stores/screenshotStore').then(({ useScreenshotStore }) => { void import('./stores/screenshotStore').then(({ useScreenshotStore }) => {
const screenshotStore = useScreenshotStore() const screenshotStore = useScreenshotStore()
screenshotStore.loadSettings() screenshotStore.loadSettings()
screenshotStore.loadHistory()
screenshotStore.initOverlay().catch(e => console.error('Screenshot overlay init error:', e)) screenshotStore.initOverlay().catch(e => console.error('Screenshot overlay init error:', e))
screenshotStore.initExportListener().catch(e => console.error('Screenshot export listener init error:', e)) screenshotStore.initExportListener().catch(e => console.error('Screenshot export listener init error:', e))
screenshotStore.initShortcutListener().catch(e => console.error('Screenshot shortcut listener init error:', e)) screenshotStore.initShortcutListener().catch(e => console.error('Screenshot shortcut listener init error:', e))
screenshotStore.initShortcutRegistration().catch(e => console.error('Screenshot shortcut registration init error:', e)) screenshotStore.initShortcutRegistration().catch(e => console.error('Screenshot shortcut registration init error:', e))
screenshotStore.initPinShortcutListener().catch(e => console.error('Screenshot pin shortcut listener init error:', e))
screenshotStore.initPinShortcutRegistration().catch(e => console.error('Screenshot pin shortcut registration init error:', e))
}) })
}) })
} }
+48 -17
View File
@@ -16,6 +16,7 @@ import { Input } from '@/components/ui/input'
import { Switch } from '@/components/ui/switch' import { Switch } from '@/components/ui/switch'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'
import { import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription,
} from '@/components/ui/dialog' } from '@/components/ui/dialog'
@@ -308,7 +309,7 @@ onUnmounted(() => {
<TabsContent value="history" class="flex-1 min-h-0 flex flex-col mt-4 tab-animate"> <TabsContent value="history" class="flex-1 min-h-0 flex flex-col mt-4 tab-animate">
<div class="flex items-center gap-2 mb-3 shrink-0"> <div class="flex items-center gap-2 mb-3 shrink-0">
<div class="relative flex-1 max-w-sm"> <div class="relative flex-1 max-w-sm">
<Search class="absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" /> <Search class="absolute left-2.5 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
<Input v-model="searchQuery" placeholder="搜索历史..." class="pl-8" /> <Input v-model="searchQuery" placeholder="搜索历史..." class="pl-8" />
</div> </div>
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
@@ -327,7 +328,7 @@ onUnmounted(() => {
{{ store.historyTotal }} {{ store.historyTotal }}
</Badge> </Badge>
<Button variant="outline" size="sm" @click="clearOpen = true" :disabled="!historyList.length"> <Button variant="outline" size="sm" @click="clearOpen = true" :disabled="!historyList.length">
<Eraser class="h-4 w-4 mr-1" />清空 <Eraser class="size-4 mr-1" />清空
</Button> </Button>
</div> </div>
@@ -372,7 +373,7 @@ onUnmounted(() => {
v-if="!historyList.length" v-if="!historyList.length"
class="flex flex-col items-center justify-center text-muted-foreground py-12" class="flex flex-col items-center justify-center text-muted-foreground py-12"
> >
<ClipboardList class="h-12 w-12 mb-3 opacity-40" /> <ClipboardList class="size-12 mb-3 opacity-40" />
<p class="text-sm">暂无历史记录复制内容后将自动收录</p> <p class="text-sm">暂无历史记录复制内容后将自动收录</p>
</div> </div>
<Card <Card
@@ -381,9 +382,9 @@ onUnmounted(() => {
class="group hover:shadow-md transition-shadow py-0" class="group hover:shadow-md transition-shadow py-0"
> >
<CardContent class="flex items-center gap-3 px-3 py-2"> <CardContent class="flex items-center gap-3 px-3 py-2">
<component :is="kindIcon(item.kind)" class="h-4 w-4 text-muted-foreground shrink-0" /> <component :is="kindIcon(item.kind)" class="size-4 text-muted-foreground shrink-0" />
<div class="flex-1 min-w-0 cursor-pointer" @click="openDetail(item)"> <div class="flex-1 min-w-0 cursor-pointer" @click="openDetail(item)">
<p class="text-sm break-all line-clamp-1" :title="item.preview">{{ item.preview }}</p> <p class="text-sm break-all line-clamp-1">{{ item.preview }}</p>
<div class="flex items-center gap-2 mt-0.5 text-xs text-muted-foreground flex-wrap"> <div class="flex items-center gap-2 mt-0.5 text-xs text-muted-foreground flex-wrap">
<Badge variant="outline" :class="['px-1.5 py-0 text-[10px] border', kindBadgeClass(item.kind)]">{{ kindLabel(item.kind) }}</Badge> <Badge variant="outline" :class="['px-1.5 py-0 text-[10px] border', kindBadgeClass(item.kind)]">{{ kindLabel(item.kind) }}</Badge>
<span>{{ formatTime(item.createdAt) }}</span> <span>{{ formatTime(item.createdAt) }}</span>
@@ -391,15 +392,30 @@ onUnmounted(() => {
</div> </div>
</div> </div>
<div class="flex items-center gap-0.5 shrink-0 opacity-0 group-hover:opacity-100 transition"> <div class="flex items-center gap-0.5 shrink-0 opacity-0 group-hover:opacity-100 transition">
<Button variant="ghost" size="icon" class="h-7 w-7" title="复制" @click.stop="handleCopy(item)"> <Tooltip>
<TooltipTrigger as-child>
<Button variant="ghost" size="icon" class="size-7" @click.stop="handleCopy(item)">
<Copy class="h-3.5 w-3.5" /> <Copy class="h-3.5 w-3.5" />
</Button> </Button>
<Button variant="ghost" size="icon" class="h-7 w-7" :title="item.pinned ? '取消固定' : '固定'" @click.stop="handlePin(item)"> </TooltipTrigger>
<TooltipContent>复制</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger as-child>
<Button variant="ghost" size="icon" class="size-7" @click.stop="handlePin(item)">
<component :is="item.pinned ? PinOff : Pin" class="h-3.5 w-3.5" /> <component :is="item.pinned ? PinOff : Pin" class="h-3.5 w-3.5" />
</Button> </Button>
<Button variant="ghost" size="icon" class="h-7 w-7 hover:text-destructive" title="删除" @click.stop="handleDelete(item)"> </TooltipTrigger>
<TooltipContent>{{ item.pinned ? '取消固定' : '固定' }}</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger as-child>
<Button variant="ghost" size="icon" class="size-7 hover:text-destructive" @click.stop="handleDelete(item)">
<Trash2 class="h-3.5 w-3.5" /> <Trash2 class="h-3.5 w-3.5" />
</Button> </Button>
</TooltipTrigger>
<TooltipContent>删除</TooltipContent>
</Tooltip>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
@@ -414,15 +430,15 @@ onUnmounted(() => {
v-if="!pinnedList.length" v-if="!pinnedList.length"
class="flex flex-col items-center justify-center h-full text-muted-foreground" class="flex flex-col items-center justify-center h-full text-muted-foreground"
> >
<Pin class="h-12 w-12 mb-3 opacity-40" /> <Pin class="size-12 mb-3 opacity-40" />
<p class="text-sm">暂无固定条目</p> <p class="text-sm">暂无固定条目</p>
<p class="text-xs mt-1">鼠标悬停历史条目点击图钉按钮即可固定</p> <p class="text-xs mt-1">鼠标悬停历史条目点击图钉按钮即可固定</p>
</div> </div>
<Card v-for="item in pinnedList" :key="item.id" class="group hover:shadow-md transition-shadow py-0"> <Card v-for="item in pinnedList" :key="item.id" class="group hover:shadow-md transition-shadow py-0">
<CardContent class="flex items-center gap-3 px-3 py-2"> <CardContent class="flex items-center gap-3 px-3 py-2">
<component :is="kindIcon(item.kind)" class="h-4 w-4 text-primary shrink-0" /> <component :is="kindIcon(item.kind)" class="size-4 text-primary shrink-0" />
<div class="flex-1 min-w-0 cursor-pointer" @click="openDetail(item)"> <div class="flex-1 min-w-0 cursor-pointer" @click="openDetail(item)">
<p class="text-sm break-all line-clamp-1" :title="item.preview">{{ item.preview }}</p> <p class="text-sm break-all line-clamp-1">{{ item.preview }}</p>
<div class="flex items-center gap-2 mt-0.5 text-xs text-muted-foreground flex-wrap"> <div class="flex items-center gap-2 mt-0.5 text-xs text-muted-foreground flex-wrap">
<Badge variant="outline" :class="['px-1.5 py-0 text-[10px] border', kindBadgeClass(item.kind)]">{{ kindLabel(item.kind) }}</Badge> <Badge variant="outline" :class="['px-1.5 py-0 text-[10px] border', kindBadgeClass(item.kind)]">{{ kindLabel(item.kind) }}</Badge>
<span>{{ formatTime(item.createdAt) }}</span> <span>{{ formatTime(item.createdAt) }}</span>
@@ -430,15 +446,30 @@ onUnmounted(() => {
</div> </div>
</div> </div>
<div class="flex items-center gap-0.5 shrink-0 opacity-0 group-hover:opacity-100 transition"> <div class="flex items-center gap-0.5 shrink-0 opacity-0 group-hover:opacity-100 transition">
<Button variant="ghost" size="icon" class="h-7 w-7" title="复制" @click.stop="handleCopy(item)"> <Tooltip>
<TooltipTrigger as-child>
<Button variant="ghost" size="icon" class="size-7" @click.stop="handleCopy(item)">
<Copy class="h-3.5 w-3.5" /> <Copy class="h-3.5 w-3.5" />
</Button> </Button>
<Button variant="ghost" size="icon" class="h-7 w-7" title="取消固定" @click.stop="handlePin(item)"> </TooltipTrigger>
<TooltipContent>复制</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger as-child>
<Button variant="ghost" size="icon" class="size-7" @click.stop="handlePin(item)">
<PinOff class="h-3.5 w-3.5" /> <PinOff class="h-3.5 w-3.5" />
</Button> </Button>
<Button variant="ghost" size="icon" class="h-7 w-7 hover:text-destructive" title="删除" @click.stop="handleDelete(item)"> </TooltipTrigger>
<TooltipContent>取消固定</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger as-child>
<Button variant="ghost" size="icon" class="size-7 hover:text-destructive" @click.stop="handleDelete(item)">
<Trash2 class="h-3.5 w-3.5" /> <Trash2 class="h-3.5 w-3.5" />
</Button> </Button>
</TooltipTrigger>
<TooltipContent>删除</TooltipContent>
</Tooltip>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
@@ -450,7 +481,7 @@ onUnmounted(() => {
<div class="max-w-xl space-y-4"> <div class="max-w-xl space-y-4">
<div class="flex items-center"> <div class="flex items-center">
<h3 class="text-base font-medium flex items-center gap-2"> <h3 class="text-base font-medium flex items-center gap-2">
<SettingsIcon class="h-4 w-4" />基本设置 <SettingsIcon class="size-4" />基本设置
</h3> </h3>
</div> </div>
@@ -519,7 +550,7 @@ onUnmounted(() => {
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
<Label class="flex items-center gap-2"> <Label class="flex items-center gap-2">
<Keyboard class="h-4 w-4" />快捷弹窗快捷键 <Keyboard class="size-4" />快捷弹窗快捷键
</Label> </Label>
<p class="text-xs text-muted-foreground mt-1">全局快捷键触发鼠标位置历史弹窗留空禁用</p> <p class="text-xs text-muted-foreground mt-1">全局快捷键触发鼠标位置历史弹窗留空禁用</p>
</div> </div>
@@ -563,7 +594,7 @@ onUnmounted(() => {
</DialogHeader> </DialogHeader>
<div class="flex-1 min-h-0 overflow-auto"> <div class="flex-1 min-h-0 overflow-auto">
<div v-if="detailLoading" class="flex items-center justify-center py-12"> <div v-if="detailLoading" class="flex items-center justify-center py-12">
<Loader2 class="h-6 w-6 animate-spin text-muted-foreground" /> <Loader2 class="size-6 animate-spin text-muted-foreground" />
</div> </div>
<template v-else-if="detail"> <template v-else-if="detail">
<img <img
+20 -7
View File
@@ -15,6 +15,7 @@ import { Input } from '@/components/ui/input'
import { import {
Pagination, PaginationContent, PaginationItem, PaginationEllipsis, Pagination, PaginationContent, PaginationItem, PaginationEllipsis,
} from '@/components/ui/pagination' } from '@/components/ui/pagination'
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from '@/components/ui/tooltip'
// ===== 与 Rust 端对应的数据结构(bindings 提供,camelCase ===== // ===== 与 Rust 端对应的数据结构(bindings 提供,camelCase =====
// kind 为 bindings 生成的 string,前端按字符串比较即可 // kind 为 bindings 生成的 string,前端按字符串比较即可
@@ -363,10 +364,11 @@ onUnmounted(() => {
<template> <template>
<div class="popup-root flex flex-col h-screen w-screen" @keydown="onKeydown"> <div class="popup-root flex flex-col h-screen w-screen" @keydown="onKeydown">
<TooltipProvider>
<!-- 搜索栏与剪切板主页统一样式 --> <!-- 搜索栏与剪切板主页统一样式 -->
<div class="flex items-center gap-2 px-3 py-2 border-b border-border shrink-0"> <div class="flex items-center gap-2 px-3 py-2 border-b border-border shrink-0">
<div class="relative flex-1 max-w-sm"> <div class="relative flex-1 max-w-sm">
<Search class="absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" /> <Search class="absolute left-2.5 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
<Input <Input
ref="searchInputRef" ref="searchInputRef"
v-model="searchQuery" v-model="searchQuery"
@@ -389,10 +391,10 @@ onUnmounted(() => {
<ScrollArea class="popup-list flex-1 min-h-0"> <ScrollArea class="popup-list flex-1 min-h-0">
<div class="space-y-1.5 p-2"> <div class="space-y-1.5 p-2">
<div v-if="loading && !hasItems" class="flex flex-col items-center justify-center py-12 text-muted-foreground"> <div v-if="loading && !hasItems" class="flex flex-col items-center justify-center py-12 text-muted-foreground">
<Loader2 class="h-6 w-6 animate-spin" /> <Loader2 class="size-6 animate-spin" />
</div> </div>
<div v-else-if="!hasItems" class="flex flex-col items-center justify-center py-12 text-muted-foreground"> <div v-else-if="!hasItems" class="flex flex-col items-center justify-center py-12 text-muted-foreground">
<ClipboardList class="h-10 w-10 mb-2 opacity-40" /> <ClipboardList class="size-10 mb-2 opacity-40" />
<p class="text-sm"> <p class="text-sm">
{{ searchQuery ? '无匹配结果' : '暂无历史记录' }} {{ searchQuery ? '无匹配结果' : '暂无历史记录' }}
</p> </p>
@@ -406,21 +408,31 @@ onUnmounted(() => {
@mouseenter="onItemHover(idx, item)" @mouseenter="onItemHover(idx, item)"
@mouseleave="onItemLeave" @mouseleave="onItemLeave"
> >
<component :is="kindIcon(item.kind)" class="h-4 w-4 text-muted-foreground shrink-0 mt-0.5" /> <component :is="kindIcon(item.kind)" class="size-4 text-muted-foreground shrink-0 mt-0.5" />
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
<p class="text-sm break-all line-clamp-1" :title="item.preview">{{ item.preview }}</p> <p class="text-sm break-all line-clamp-1">{{ item.preview }}</p>
<div class="flex items-center gap-2 mt-0.5 text-xs text-muted-foreground flex-wrap"> <div class="flex items-center gap-2 mt-0.5 text-xs text-muted-foreground flex-wrap">
<span class="popup-item-kind px-1.5 py-0 text-[10px] border rounded-sm" :class="kindBadgeClass(item.kind)">{{ kindLabel(item.kind) }}</span> <span class="popup-item-kind px-1.5 py-0 text-[10px] border rounded-sm" :class="kindBadgeClass(item.kind)">{{ kindLabel(item.kind) }}</span>
<span>{{ formatTime(item.createdAt) }}</span> <span>{{ formatTime(item.createdAt) }}</span>
</div> </div>
</div> </div>
<div class="popup-item-actions shrink-0"> <div class="popup-item-actions shrink-0">
<button class="popup-action-btn h-7 w-7" title="固定" @click="togglePin(item, $event)"> <Tooltip>
<TooltipTrigger as-child>
<button class="popup-action-btn size-7" @click="togglePin(item, $event)">
<component :is="item.pinned ? PinOff : Pin" class="h-3.5 w-3.5" /> <component :is="item.pinned ? PinOff : Pin" class="h-3.5 w-3.5" />
</button> </button>
<button class="popup-action-btn h-7 w-7 hover:text-destructive" title="删除" @click="deleteItem(item, $event)"> </TooltipTrigger>
<TooltipContent>固定</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger as-child>
<button class="popup-action-btn size-7 hover:text-destructive" @click="deleteItem(item, $event)">
<Trash2 class="h-3.5 w-3.5" /> <Trash2 class="h-3.5 w-3.5" />
</button> </button>
</TooltipTrigger>
<TooltipContent>删除</TooltipContent>
</Tooltip>
</div> </div>
</div> </div>
</div> </div>
@@ -460,6 +472,7 @@ onUnmounted(() => {
<span><kbd>Enter</kbd> 粘贴</span> <span><kbd>Enter</kbd> 粘贴</span>
<span><kbd>Esc</kbd> 关闭</span> <span><kbd>Esc</kbd> 关闭</span>
</div> </div>
</TooltipProvider>
</div> </div>
</template> </template>
+68 -83
View File
@@ -616,15 +616,15 @@ const toggleSortOrder = () => {
<div ref="tabsListRef"> <div ref="tabsListRef">
<TabsList class="grid w-full grid-cols-3 max-w-md !bg-transparent !p-0 !shadow-none"> <TabsList class="grid w-full grid-cols-3 max-w-md !bg-transparent !p-0 !shadow-none">
<TabsTrigger value="tasks" class="gap-1.5"> <TabsTrigger value="tasks" class="gap-1.5">
<ListChecks class="h-4 w-4" /> <ListChecks class="size-4" />
下载任务 下载任务
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="settings" class="gap-1.5"> <TabsTrigger value="settings" class="gap-1.5">
<SettingsIcon class="h-4 w-4" /> <SettingsIcon class="size-4" />
设置 设置
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="extension" class="gap-1.5"> <TabsTrigger value="extension" class="gap-1.5">
<Puzzle class="h-4 w-4" /> <Puzzle class="size-4" />
浏览器扩展 浏览器扩展
</TabsTrigger> </TabsTrigger>
</TabsList> </TabsList>
@@ -650,19 +650,19 @@ const toggleSortOrder = () => {
<Separator orientation="vertical" class="h-5" /> <Separator orientation="vertical" class="h-5" />
<div v-if="running" class="flex items-center gap-2 text-xs"> <div v-if="running" class="flex items-center gap-2 text-xs">
<Badge variant="secondary" class="gap-1"> <Badge variant="secondary" class="gap-1">
<Download class="h-3 w-3" /> <Download class="size-3" />
下载中 {{ statusCounts.active }} 下载中 {{ statusCounts.active }}
</Badge> </Badge>
<Badge variant="secondary" class="gap-1"> <Badge variant="secondary" class="gap-1">
<Clock class="h-3 w-3" /> <Clock class="size-3" />
等待 {{ statusCounts.queued }} 等待 {{ statusCounts.queued }}
</Badge> </Badge>
<Badge v-if="statusCounts.paused > 0" variant="secondary" class="gap-1"> <Badge v-if="statusCounts.paused > 0" variant="secondary" class="gap-1">
<Pause class="h-3 w-3" /> <Pause class="size-3" />
已暂停 {{ statusCounts.paused }} 已暂停 {{ statusCounts.paused }}
</Badge> </Badge>
<Badge variant="secondary" class="gap-1"> <Badge variant="secondary" class="gap-1">
<Check class="h-3 w-3" /> <Check class="size-3" />
已完成 {{ statusCounts.complete }} 已完成 {{ statusCounts.complete }}
</Badge> </Badge>
</div> </div>
@@ -673,7 +673,7 @@ const toggleSortOrder = () => {
variant="ghost" variant="ghost"
@click="store.refreshTasks()" @click="store.refreshTasks()"
> >
<RefreshCw class="h-4 w-4" /> <RefreshCw class="size-4" />
</Button> </Button>
</div> </div>
</div> </div>
@@ -683,20 +683,15 @@ const toggleSortOrder = () => {
<!-- 工具栏筛选 / 排序 / 搜索 --> <!-- 工具栏筛选 / 排序 / 搜索 -->
<div class="shrink-0 flex items-center gap-2 flex-wrap"> <div class="shrink-0 flex items-center gap-2 flex-wrap">
<!-- 新建下载按钮 --> <!-- 新建下载按钮 -->
<Tooltip>
<TooltipTrigger as-child>
<Button <Button
size="sm" size="sm"
class="h-8 gap-1.5" class="h-8 gap-1.5"
:disabled="addingTask" :disabled="addingTask"
@click="addDialogOpen = true" @click="addDialogOpen = true"
> >
<Plus class="h-4 w-4" /> <Plus class="size-4" />
新建下载 新建下载
</Button> </Button>
</TooltipTrigger>
<TooltipContent>添加下载链接</TooltipContent>
</Tooltip>
<!-- 状态筛选 --> <!-- 状态筛选 -->
<Select v-model="statusFilter"> <Select v-model="statusFilter">
@@ -736,11 +731,11 @@ const toggleSortOrder = () => {
<Button <Button
size="sm" size="sm"
variant="outline" variant="outline"
class="h-8 w-8 p-0 bg-transparent" class="size-8 p-0 bg-transparent"
@click="toggleSortOrder" @click="toggleSortOrder"
> >
<ArrowUp v-if="sortOrder === 'asc'" class="h-4 w-4" /> <ArrowUp v-if="sortOrder === 'asc'" class="size-4" />
<ArrowDown v-else class="h-4 w-4" /> <ArrowDown v-else class="size-4" />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>{{ sortOrder === 'asc' ? '升序(点击切换降序)' : '降序(点击切换升序)' }}</TooltipContent> <TooltipContent>{{ sortOrder === 'asc' ? '升序(点击切换降序)' : '降序(点击切换升序)' }}</TooltipContent>
@@ -748,7 +743,7 @@ const toggleSortOrder = () => {
<!-- 搜索框 --> <!-- 搜索框 -->
<div class="relative flex-1 min-w-[180px] max-w-xs"> <div class="relative flex-1 min-w-[180px] max-w-xs">
<Search class="absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" /> <Search class="absolute left-2 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
<Input <Input
v-model="searchText" v-model="searchText"
placeholder="搜索文件名..." placeholder="搜索文件名..."
@@ -756,20 +751,15 @@ const toggleSortOrder = () => {
/> />
</div> </div>
<Tooltip>
<TooltipTrigger as-child>
<Button <Button
size="sm" size="sm"
variant="outline" variant="outline"
class="h-8 gap-1.5 bg-transparent" class="h-8 gap-1.5 bg-transparent"
@click="downloadSettingsOpen = true" @click="downloadSettingsOpen = true"
> >
<SettingsIcon class="h-4 w-4" /> <SettingsIcon class="size-4" />
下载设置 下载设置
</Button> </Button>
</TooltipTrigger>
<TooltipContent>下载设置</TooltipContent>
</Tooltip>
<span class="text-xs text-muted-foreground ml-auto"> <span class="text-xs text-muted-foreground ml-auto">
{{ sortedTasks.length }} 个任务 {{ sortedTasks.length }} 个任务
@@ -779,10 +769,10 @@ const toggleSortOrder = () => {
<!-- 任务列表 --> <!-- 任务列表 -->
<ScrollArea class="flex-1 min-h-0"> <ScrollArea class="flex-1 min-h-0">
<div v-if="pagedTasks.length === 0" key="empty" class="h-full flex flex-col items-center justify-center text-muted-foreground gap-2 pt-10"> <div v-if="pagedTasks.length === 0" key="empty" class="h-full flex flex-col items-center justify-center text-muted-foreground gap-2 pt-10">
<CheckCircle2 class="h-16 w-16 opacity-30" /> <CheckCircle2 class="size-16 opacity-30" />
<p>暂无符合条件的任务</p> <p>暂无符合条件的任务</p>
<Button size="sm" class="mt-2" @click="addDialogOpen = true"> <Button size="sm" class="mt-2" @click="addDialogOpen = true">
<Plus class="h-4 w-4" /> <Plus class="size-4" />
新建下载 新建下载
</Button> </Button>
</div> </div>
@@ -792,19 +782,14 @@ const toggleSortOrder = () => {
<div class="flex items-start justify-between gap-3 mb-3"> <div class="flex items-start justify-between gap-3 mb-3">
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-1"> <div class="flex items-center gap-2 mb-1">
<Tooltip>
<TooltipTrigger as-child>
<span class="font-medium truncate cursor-default"> <span class="font-medium truncate cursor-default">
{{ getFileName(task) }} {{ getFileName(task) }}
</span> </span>
</TooltipTrigger>
<TooltipContent>{{ getFileName(task) }}</TooltipContent>
</Tooltip>
<Badge <Badge
:variant="getTaskStatusBadge(task).variant" :variant="getTaskStatusBadge(task).variant"
class="shrink-0" class="shrink-0"
> >
<component :is="getTaskStatusBadge(task).icon" class="h-3 w-3" /> <component :is="getTaskStatusBadge(task).icon" class="size-3" />
{{ getTaskStatusBadge(task).text }} {{ getTaskStatusBadge(task).text }}
</Badge> </Badge>
</div> </div>
@@ -813,7 +798,7 @@ const toggleSortOrder = () => {
{{ formatSize(task.completedSize) }} / {{ formatSize(task.totalSize) }} {{ formatSize(task.completedSize) }} / {{ formatSize(task.totalSize) }}
</span> </span>
<span v-if="task.status === 'active'" class="flex items-center gap-1 text-green-600 dark:text-green-400"> <span v-if="task.status === 'active'" class="flex items-center gap-1 text-green-600 dark:text-green-400">
<Download class="h-3 w-3" /> <Download class="size-3" />
{{ formatSpeed(task.speed) }} {{ formatSpeed(task.speed) }}
</span> </span>
<Badge v-if="task.status === 'active' && task.segments.length > 1" variant="outline" class="gap-1 py-0 px-1.5 text-[10px]"> <Badge v-if="task.status === 'active' && task.segments.length > 1" variant="outline" class="gap-1 py-0 px-1.5 text-[10px]">
@@ -826,7 +811,7 @@ const toggleSortOrder = () => {
{{ truncatePath(task.dir) }} {{ truncatePath(task.dir) }}
</span> </span>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent class="max-w-[400px] break-all">{{ task.dir }}</TooltipContent> <TooltipContent class="max-w-[480px] break-words">{{ task.dir }}</TooltipContent>
</Tooltip> </Tooltip>
<span v-if="task.error" class="text-destructive"> <span v-if="task.error" class="text-destructive">
{{ task.error }} {{ task.error }}
@@ -839,10 +824,10 @@ const toggleSortOrder = () => {
<Button <Button
size="icon" size="icon"
variant="ghost" variant="ghost"
class="h-8 w-8" class="size-8"
@click="handlePause(task.id)" @click="handlePause(task.id)"
> >
<Pause class="h-4 w-4" /> <Pause class="size-4" />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>{{ task.status === 'queued' ? '取消排队' : '暂停' }}</TooltipContent> <TooltipContent>{{ task.status === 'queued' ? '取消排队' : '暂停' }}</TooltipContent>
@@ -852,10 +837,10 @@ const toggleSortOrder = () => {
<Button <Button
size="icon" size="icon"
variant="ghost" variant="ghost"
class="h-8 w-8" class="size-8"
@click="handleResume(task.id)" @click="handleResume(task.id)"
> >
<Play class="h-4 w-4" /> <Play class="size-4" />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>继续</TooltipContent> <TooltipContent>继续</TooltipContent>
@@ -865,10 +850,10 @@ const toggleSortOrder = () => {
<Button <Button
size="icon" size="icon"
variant="ghost" variant="ghost"
class="h-8 w-8" class="size-8"
@click="handleShowDetail(task)" @click="handleShowDetail(task)"
> >
<Info class="h-4 w-4" /> <Info class="size-4" />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>详细信息</TooltipContent> <TooltipContent>详细信息</TooltipContent>
@@ -878,10 +863,10 @@ const toggleSortOrder = () => {
<Button <Button
size="icon" size="icon"
variant="ghost" variant="ghost"
class="h-8 w-8" class="size-8"
@click="handleOpenDir(task)" @click="handleOpenDir(task)"
> >
<FolderOpen class="h-4 w-4" /> <FolderOpen class="size-4" />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>打开目录</TooltipContent> <TooltipContent>打开目录</TooltipContent>
@@ -891,10 +876,10 @@ const toggleSortOrder = () => {
<Button <Button
size="icon" size="icon"
variant="ghost" variant="ghost"
class="h-8 w-8 text-destructive hover:text-destructive" class="size-8 text-destructive hover:text-destructive"
@click="handleRemove(task)" @click="handleRemove(task)"
> >
<Trash2 class="h-4 w-4" /> <Trash2 class="size-4" />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>删除</TooltipContent> <TooltipContent>删除</TooltipContent>
@@ -919,12 +904,12 @@ const toggleSortOrder = () => {
<Button <Button
size="sm" size="sm"
variant="outline" variant="outline"
class="h-8 w-8 p-0" class="size-8 p-0"
:disabled="currentPage <= 1" :disabled="currentPage <= 1"
@click="currentPage = 1" @click="currentPage = 1"
> >
<ChevronLeft class="h-4 w-4" /> <ChevronLeft class="size-4" />
<ChevronLeft class="h-4 w-4 -ml-2" /> <ChevronLeft class="size-4 -ml-2" />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>第一页</TooltipContent> <TooltipContent>第一页</TooltipContent>
@@ -934,11 +919,11 @@ const toggleSortOrder = () => {
<Button <Button
size="sm" size="sm"
variant="outline" variant="outline"
class="h-8 w-8 p-0" class="size-8 p-0"
:disabled="currentPage <= 1" :disabled="currentPage <= 1"
@click="currentPage--" @click="currentPage--"
> >
<ChevronLeft class="h-4 w-4" /> <ChevronLeft class="size-4" />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>上一页</TooltipContent> <TooltipContent>上一页</TooltipContent>
@@ -951,11 +936,11 @@ const toggleSortOrder = () => {
<Button <Button
size="sm" size="sm"
variant="outline" variant="outline"
class="h-8 w-8 p-0" class="size-8 p-0"
:disabled="currentPage >= totalPages" :disabled="currentPage >= totalPages"
@click="currentPage++" @click="currentPage++"
> >
<ChevronRight class="h-4 w-4" /> <ChevronRight class="size-4" />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>下一页</TooltipContent> <TooltipContent>下一页</TooltipContent>
@@ -965,12 +950,12 @@ const toggleSortOrder = () => {
<Button <Button
size="sm" size="sm"
variant="outline" variant="outline"
class="h-8 w-8 p-0" class="size-8 p-0"
:disabled="currentPage >= totalPages" :disabled="currentPage >= totalPages"
@click="currentPage = totalPages" @click="currentPage = totalPages"
> >
<ChevronRight class="h-4 w-4" /> <ChevronRight class="size-4" />
<ChevronRight class="h-4 w-4 -ml-2" /> <ChevronRight class="size-4 -ml-2" />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>最后一页</TooltipContent> <TooltipContent>最后一页</TooltipContent>
@@ -988,7 +973,7 @@ const toggleSortOrder = () => {
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle class="flex items-center gap-2 text-base"> <CardTitle class="flex items-center gap-2 text-base">
<FolderOpen class="h-4 w-4 text-primary" /> <FolderOpen class="size-4 text-primary" />
下载设置 下载设置
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
@@ -1004,7 +989,7 @@ const toggleSortOrder = () => {
<Tooltip> <Tooltip>
<TooltipTrigger as-child> <TooltipTrigger as-child>
<Button variant="outline" size="icon" @click="handleSelectSettingsDir"> <Button variant="outline" size="icon" @click="handleSelectSettingsDir">
<FolderOpen class="h-4 w-4" /> <FolderOpen class="size-4" />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>选择目录</TooltipContent> <TooltipContent>选择目录</TooltipContent>
@@ -1079,7 +1064,7 @@ const toggleSortOrder = () => {
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle class="flex items-center gap-2 text-base"> <CardTitle class="flex items-center gap-2 text-base">
<Zap class="h-4 w-4 text-primary" /> <Zap class="size-4 text-primary" />
扩展 API 扩展 API
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
@@ -1121,7 +1106,7 @@ const toggleSortOrder = () => {
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle class="flex items-center gap-2 text-base"> <CardTitle class="flex items-center gap-2 text-base">
<Puzzle class="h-4 w-4 text-primary" /> <Puzzle class="size-4 text-primary" />
Thing Extension Thing Extension
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
@@ -1142,7 +1127,7 @@ const toggleSortOrder = () => {
{{ store.extensionInfo.url }} {{ store.extensionInfo.url }}
</span> </span>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent class="max-w-[400px] break-all">{{ store.extensionInfo.url }}</TooltipContent> <TooltipContent class="max-w-[480px] break-words">{{ store.extensionInfo.url }}</TooltipContent>
</Tooltip> </Tooltip>
</div> </div>
<Tooltip> <Tooltip>
@@ -1150,10 +1135,10 @@ const toggleSortOrder = () => {
<Button <Button
size="icon" size="icon"
variant="ghost" variant="ghost"
class="h-8 w-8 shrink-0" class="size-8 shrink-0"
@click="handleCopy(store.extensionInfo.url, 'API 地址')" @click="handleCopy(store.extensionInfo.url, 'API 地址')"
> >
<Copy class="h-4 w-4" /> <Copy class="size-4" />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>复制 API 地址</TooltipContent> <TooltipContent>复制 API 地址</TooltipContent>
@@ -1173,11 +1158,11 @@ const toggleSortOrder = () => {
<Button <Button
size="icon" size="icon"
variant="ghost" variant="ghost"
class="h-8 w-8" class="size-8"
@click="showSecret = !showSecret" @click="showSecret = !showSecret"
> >
<Eye v-if="!showSecret" class="h-4 w-4" /> <Eye v-if="!showSecret" class="size-4" />
<EyeOff v-else class="h-4 w-4" /> <EyeOff v-else class="size-4" />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>{{ showSecret ? '隐藏' : '显示' }}</TooltipContent> <TooltipContent>{{ showSecret ? '隐藏' : '显示' }}</TooltipContent>
@@ -1187,10 +1172,10 @@ const toggleSortOrder = () => {
<Button <Button
size="icon" size="icon"
variant="ghost" variant="ghost"
class="h-8 w-8" class="size-8"
@click="handleCopy(store.extensionInfo.secret, '密钥')" @click="handleCopy(store.extensionInfo.secret, '密钥')"
> >
<Copy class="h-4 w-4" /> <Copy class="size-4" />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>复制密钥</TooltipContent> <TooltipContent>复制密钥</TooltipContent>
@@ -1205,7 +1190,7 @@ const toggleSortOrder = () => {
<Label class="text-xs text-muted-foreground">安装扩展</Label> <Label class="text-xs text-muted-foreground">安装扩展</Label>
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
<Button variant="outline" @click="handleInstallExtensionOnline"> <Button variant="outline" @click="handleInstallExtensionOnline">
<ExternalLink class="h-4 w-4" /> <ExternalLink class="size-4" />
在线安装 在线安装
</Button> </Button>
</div> </div>
@@ -1235,7 +1220,7 @@ const toggleSortOrder = () => {
<DialogContent class="max-w-lg"> <DialogContent class="max-w-lg">
<DialogHeader> <DialogHeader>
<DialogTitle class="flex items-center gap-2"> <DialogTitle class="flex items-center gap-2">
<SettingsIcon class="h-4 w-4 text-primary" /> <SettingsIcon class="size-4 text-primary" />
下载设置 下载设置
</DialogTitle> </DialogTitle>
<DialogDescription class="text-xs"> <DialogDescription class="text-xs">
@@ -1256,7 +1241,7 @@ const toggleSortOrder = () => {
<Tooltip> <Tooltip>
<TooltipTrigger as-child> <TooltipTrigger as-child>
<Button variant="outline" size="icon" @click="handleSelectSettingsDir"> <Button variant="outline" size="icon" @click="handleSelectSettingsDir">
<FolderOpen class="h-4 w-4" /> <FolderOpen class="size-4" />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>选择目录</TooltipContent> <TooltipContent>选择目录</TooltipContent>
@@ -1328,7 +1313,7 @@ const toggleSortOrder = () => {
<Button variant="outline">取消</Button> <Button variant="outline">取消</Button>
</DialogClose> </DialogClose>
<Button @click="handleDialogSave"> <Button @click="handleDialogSave">
<Check class="h-4 w-4" /> <Check class="size-4" />
保存 保存
</Button> </Button>
</DialogFooter> </DialogFooter>
@@ -1340,7 +1325,7 @@ const toggleSortOrder = () => {
<DialogContent class="max-w-lg"> <DialogContent class="max-w-lg">
<DialogHeader> <DialogHeader>
<DialogTitle class="flex items-center gap-2"> <DialogTitle class="flex items-center gap-2">
<Link2 class="h-4 w-4 text-primary" /> <Link2 class="size-4 text-primary" />
新建下载 新建下载
</DialogTitle> </DialogTitle>
<DialogDescription class="text-xs"> <DialogDescription class="text-xs">
@@ -1371,7 +1356,7 @@ const toggleSortOrder = () => {
<Tooltip> <Tooltip>
<TooltipTrigger as-child> <TooltipTrigger as-child>
<Button variant="outline" size="icon" @click="handleSelectDir"> <Button variant="outline" size="icon" @click="handleSelectDir">
<FolderOpen class="h-4 w-4" /> <FolderOpen class="size-4" />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>选择目录</TooltipContent> <TooltipContent>选择目录</TooltipContent>
@@ -1389,8 +1374,8 @@ const toggleSortOrder = () => {
<Button variant="outline">取消</Button> <Button variant="outline">取消</Button>
</DialogClose> </DialogClose>
<Button :disabled="addingTask || !addUriText.trim()" @click="handleAddDownload"> <Button :disabled="addingTask || !addUriText.trim()" @click="handleAddDownload">
<Loader2 v-if="addingTask" class="h-4 w-4 animate-spin" /> <Loader2 v-if="addingTask" class="size-4 animate-spin" />
<Plus v-else class="h-4 w-4" /> <Plus v-else class="size-4" />
添加下载 添加下载
</Button> </Button>
</DialogFooter> </DialogFooter>
@@ -1402,7 +1387,7 @@ const toggleSortOrder = () => {
<AlertDialogContent class="max-w-md"> <AlertDialogContent class="max-w-md">
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle class="flex items-center gap-2"> <AlertDialogTitle class="flex items-center gap-2">
<AlertCircle class="h-4 w-4 text-amber-500" /> <AlertCircle class="size-4 text-amber-500" />
下载重复提醒 下载重复提醒
</AlertDialogTitle> </AlertDialogTitle>
<AlertDialogDescription class="text-xs"> <AlertDialogDescription class="text-xs">
@@ -1423,7 +1408,7 @@ const toggleSortOrder = () => {
<AlertDialogCancel @click="onDuplicateCancel">取消全部</AlertDialogCancel> <AlertDialogCancel @click="onDuplicateCancel">取消全部</AlertDialogCancel>
<Button variant="outline" @click="onDuplicateSkip">跳过此链接</Button> <Button variant="outline" @click="onDuplicateSkip">跳过此链接</Button>
<AlertDialogAction @click="onDuplicateConfirm"> <AlertDialogAction @click="onDuplicateConfirm">
<Download class="h-4 w-4" /> <Download class="size-4" />
仍然下载 仍然下载
</AlertDialogAction> </AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>
@@ -1469,7 +1454,7 @@ const toggleSortOrder = () => {
<DialogContent class="max-w-xl max-h-[80vh]"> <DialogContent class="max-w-xl max-h-[80vh]">
<DialogHeader> <DialogHeader>
<DialogTitle class="flex items-center gap-2"> <DialogTitle class="flex items-center gap-2">
<Info class="h-4 w-4 text-primary" /> <Info class="size-4 text-primary" />
任务详情 任务详情
</DialogTitle> </DialogTitle>
<DialogDescription class="text-xs"> <DialogDescription class="text-xs">
@@ -1487,7 +1472,7 @@ const toggleSortOrder = () => {
</div> </div>
<Tooltip> <Tooltip>
<TooltipTrigger as-child> <TooltipTrigger as-child>
<Button size="icon" variant="ghost" class="h-7 w-7 shrink-0" @click="handleCopyText(detailTask.filename, '文件名')"> <Button size="icon" variant="ghost" class="size-7 shrink-0" @click="handleCopyText(detailTask.filename, '文件名')">
<Copy class="h-3.5 w-3.5" /> <Copy class="h-3.5 w-3.5" />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
@@ -1501,7 +1486,7 @@ const toggleSortOrder = () => {
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<span class="text-xs text-muted-foreground">状态</span> <span class="text-xs text-muted-foreground">状态</span>
<Badge :variant="getTaskStatusBadge(detailTask).variant" class="gap-1"> <Badge :variant="getTaskStatusBadge(detailTask).variant" class="gap-1">
<component :is="getTaskStatusBadge(detailTask).icon" class="h-3 w-3" /> <component :is="getTaskStatusBadge(detailTask).icon" class="size-3" />
{{ getTaskStatusBadge(detailTask).text }} {{ getTaskStatusBadge(detailTask).text }}
</Badge> </Badge>
</div> </div>
@@ -1514,7 +1499,7 @@ const toggleSortOrder = () => {
</div> </div>
<Tooltip> <Tooltip>
<TooltipTrigger as-child> <TooltipTrigger as-child>
<Button size="icon" variant="ghost" class="h-7 w-7 shrink-0" @click="handleCopyText(detailTask.url, '下载链接')"> <Button size="icon" variant="ghost" class="size-7 shrink-0" @click="handleCopyText(detailTask.url, '下载链接')">
<Copy class="h-3.5 w-3.5" /> <Copy class="h-3.5 w-3.5" />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
@@ -1532,13 +1517,13 @@ const toggleSortOrder = () => {
<TooltipTrigger as-child> <TooltipTrigger as-child>
<span class="font-mono text-xs break-all cursor-default">{{ detailTask.dir }}</span> <span class="font-mono text-xs break-all cursor-default">{{ detailTask.dir }}</span>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent class="max-w-[400px] break-all">{{ detailTask.dir }}</TooltipContent> <TooltipContent class="max-w-[480px] break-words">{{ detailTask.dir }}</TooltipContent>
</Tooltip> </Tooltip>
<span class="font-mono text-xs text-muted-foreground break-all">{{ detailTask.filename }}</span> <span class="font-mono text-xs text-muted-foreground break-all">{{ detailTask.filename }}</span>
</div> </div>
<Tooltip> <Tooltip>
<TooltipTrigger as-child> <TooltipTrigger as-child>
<Button size="icon" variant="ghost" class="h-7 w-7 shrink-0" @click="handleCopyText(detailTask.dir + '\\' + detailTask.filename, '完整路径')"> <Button size="icon" variant="ghost" class="size-7 shrink-0" @click="handleCopyText(detailTask.dir + '\\' + detailTask.filename, '完整路径')">
<Copy class="h-3.5 w-3.5" /> <Copy class="h-3.5 w-3.5" />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
@@ -1643,7 +1628,7 @@ const toggleSortOrder = () => {
<Button variant="outline">关闭</Button> <Button variant="outline">关闭</Button>
</DialogClose> </DialogClose>
<Button v-if="detailTask?.dir" variant="outline" @click="handleOpenDir(detailTask)"> <Button v-if="detailTask?.dir" variant="outline" @click="handleOpenDir(detailTask)">
<FolderOpen class="h-4 w-4" /> <FolderOpen class="size-4" />
打开目录 打开目录
</Button> </Button>
</DialogFooter> </DialogFooter>
+55 -9
View File
@@ -1145,7 +1145,12 @@ watch(() => store.status?.ready, (ready, prev) => {
<CardHeader class="pb-1.5 px-3.5 pt-2.5"> <CardHeader class="pb-1.5 px-3.5 pt-2.5">
<CardTitle class="flex items-center justify-between text-sm"> <CardTitle class="flex items-center justify-between text-sm">
<span class="flex items-center gap-1.5"><Cpu class="size-4 text-primary" />CPU</span> <span class="flex items-center gap-1.5"><Cpu class="size-4 text-primary" />CPU</span>
<span class="text-xs text-muted-foreground font-normal truncate ml-2" :title="cpuModel ?? ''">{{ cpuModel ?? '--' }}</span> <Tooltip>
<TooltipTrigger as-child>
<span class="text-xs text-muted-foreground font-normal truncate ml-2">{{ cpuModel ?? '--' }}</span>
</TooltipTrigger>
<TooltipContent>{{ cpuModel ?? '' }}</TooltipContent>
</Tooltip>
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent class="px-3.5 pb-2.5 space-y-1.5"> <CardContent class="px-3.5 pb-2.5 space-y-1.5">
@@ -1178,7 +1183,12 @@ watch(() => store.status?.ready, (ready, prev) => {
<CardHeader class="pb-1.5 px-3.5 pt-2.5"> <CardHeader class="pb-1.5 px-3.5 pt-2.5">
<CardTitle class="flex items-center justify-between text-sm"> <CardTitle class="flex items-center justify-between text-sm">
<span class="flex items-center gap-1.5"><Gauge class="size-4 text-primary" />GPU</span> <span class="flex items-center gap-1.5"><Gauge class="size-4 text-primary" />GPU</span>
<span class="text-xs text-muted-foreground font-normal truncate ml-2" :title="gpuModel ?? ''">{{ gpuModel ?? '--' }}</span> <Tooltip>
<TooltipTrigger as-child>
<span class="text-xs text-muted-foreground font-normal truncate ml-2">{{ gpuModel ?? '--' }}</span>
</TooltipTrigger>
<TooltipContent>{{ gpuModel ?? '' }}</TooltipContent>
</Tooltip>
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent class="px-3.5 pb-2.5 space-y-1.5"> <CardContent class="px-3.5 pb-2.5 space-y-1.5">
@@ -1211,9 +1221,14 @@ watch(() => store.status?.ready, (ready, prev) => {
<CardHeader class="pb-1.5 px-3.5 pt-2.5"> <CardHeader class="pb-1.5 px-3.5 pt-2.5">
<CardTitle class="flex items-center justify-between text-sm"> <CardTitle class="flex items-center justify-between text-sm">
<span class="flex items-center gap-1.5"><MemoryStick class="size-4 text-primary" />内存</span> <span class="flex items-center gap-1.5"><MemoryStick class="size-4 text-primary" />内存</span>
<span class="text-xs text-muted-foreground font-normal truncate ml-2" :title="memModuleModels.join(', ')"> <Tooltip>
<TooltipTrigger as-child>
<span class="text-xs text-muted-foreground font-normal truncate ml-2">
{{ memTotalGB != null ? fmt(memTotalGB, 0) + ' GB' : '--' }} {{ memTotalGB != null ? fmt(memTotalGB, 0) + ' GB' : '--' }}
</span> </span>
</TooltipTrigger>
<TooltipContent>{{ memModuleModels.join(', ') }}</TooltipContent>
</Tooltip>
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent class="px-3.5 pb-2.5 space-y-1.5"> <CardContent class="px-3.5 pb-2.5 space-y-1.5">
@@ -1275,7 +1290,12 @@ watch(() => store.status?.ready, (ready, prev) => {
<div v-if="storageDrives.length" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2"> <div v-if="storageDrives.length" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2">
<div v-for="drive in storageDrives" :key="drive.name" class="border rounded-md p-2 space-y-1"> <div v-for="drive in storageDrives" :key="drive.name" class="border rounded-md p-2 space-y-1">
<div class="flex items-center justify-between gap-2"> <div class="flex items-center justify-between gap-2">
<span class="text-xs font-medium truncate" :title="drive.name">{{ drive.name }}</span> <Tooltip>
<TooltipTrigger as-child>
<span class="text-xs font-medium truncate">{{ drive.name }}</span>
</TooltipTrigger>
<TooltipContent>{{ drive.name }}</TooltipContent>
</Tooltip>
<span :class="['text-xs font-mono tabular-nums shrink-0', tempColor(drive.temp)]">{{ fmt(drive.temp, 0) }}°C</span> <span :class="['text-xs font-mono tabular-nums shrink-0', tempColor(drive.temp)]">{{ fmt(drive.temp, 0) }}°C</span>
</div> </div>
<div> <div>
@@ -1330,14 +1350,24 @@ watch(() => store.status?.ready, (ready, prev) => {
<Play class="size-3" />启动 <Play class="size-3" />启动
</Button> </Button>
<!-- 提权按钮标志未启用时显示设置标志 + 以管理员权限重启 Thing --> <!-- 提权按钮标志未启用时显示设置标志 + 以管理员权限重启 Thing -->
<Button v-if="!store.elevateOnLaunch" size="xs" variant="outline" class="gap-1 text-emerald-600 dark:text-emerald-400 border-emerald-500/40 hover:bg-emerald-500/10" :disabled="store.starting" title="以管理员权限重启 Thing(弹 UAC,ThingHK 子进程继承权限,后续启动自动提权,崩溃自动重启)" @click="handleElevateSelf"> <Tooltip v-if="!store.elevateOnLaunch">
<TooltipTrigger as-child>
<Button size="xs" variant="outline" class="gap-1 text-emerald-600 dark:text-emerald-400 border-emerald-500/40 hover:bg-emerald-500/10" :disabled="store.starting" @click="handleElevateSelf">
<Loader2 v-if="store.starting" class="size-3 animate-spin" /> <Loader2 v-if="store.starting" class="size-3 animate-spin" />
<ShieldCheck v-else class="size-3" />提权 <ShieldCheck v-else class="size-3" />提权
</Button> </Button>
</TooltipTrigger>
<TooltipContent class="max-w-[480px] break-words">以管理员权限重启 Thing UACThingHK 子进程继承权限后续启动自动提权崩溃自动重启</TooltipContent>
</Tooltip>
<!-- 取消提权按钮标志已启用时显示清除标志下次启动不触发 UAC --> <!-- 取消提权按钮标志已启用时显示清除标志下次启动不触发 UAC -->
<Button v-else size="xs" variant="outline" class="gap-1 text-muted-foreground hover:text-foreground" title="取消提权,下次启动将以普通权限运行(不影响当前会话)" @click="handleCancelElevation"> <Tooltip v-else>
<TooltipTrigger as-child>
<Button size="xs" variant="outline" class="gap-1 text-muted-foreground hover:text-foreground" @click="handleCancelElevation">
<ShieldOff class="size-3" />取消提权 <ShieldOff class="size-3" />取消提权
</Button> </Button>
</TooltipTrigger>
<TooltipContent class="max-w-[480px] break-words">取消提权下次启动将以普通权限运行不影响当前会话</TooltipContent>
</Tooltip>
<Button size="xs" variant="outline" :disabled="store.starting || store.stopping || store.connState === 'idle'" @click="handleRefresh"> <Button size="xs" variant="outline" :disabled="store.starting || store.stopping || store.connState === 'idle'" @click="handleRefresh">
<RefreshCw class="size-3" />刷新 <RefreshCw class="size-3" />刷新
</Button> </Button>
@@ -1420,7 +1450,12 @@ watch(() => store.status?.ready, (ready, prev) => {
<div class="text-[11px] uppercase tracking-wide text-muted-foreground/70 mb-1">{{ typeLabel(typeGroup.type) }}</div> <div class="text-[11px] uppercase tracking-wide text-muted-foreground/70 mb-1">{{ typeLabel(typeGroup.type) }}</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-0.5 text-xs"> <div class="grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-0.5 text-xs">
<div v-for="s in typeGroup.items" :key="s.id" class="flex justify-between items-center py-0.5"> <div v-for="s in typeGroup.items" :key="s.id" class="flex justify-between items-center py-0.5">
<span class="text-muted-foreground truncate pr-2" :title="s.name">{{ s.name }}</span> <Tooltip>
<TooltipTrigger as-child>
<span class="text-muted-foreground truncate pr-2">{{ s.name }}</span>
</TooltipTrigger>
<TooltipContent>{{ s.name }}</TooltipContent>
</Tooltip>
<span class="font-mono tabular-nums shrink-0" :class="{ 'text-muted-foreground/50': s.value == null }"> <span class="font-mono tabular-nums shrink-0" :class="{ 'text-muted-foreground/50': s.value == null }">
{{ s.value == null ? 'N/A' : s.value.toFixed(s.type === 'voltage' || s.type === 'power' ? 2 : 1) }} {{ s.value == null ? 'N/A' : s.value.toFixed(s.type === 'voltage' || s.type === 'power' ? 2 : 1) }}
<span class="text-muted-foreground ml-0.5">{{ s.unit }}</span> <span class="text-muted-foreground ml-0.5">{{ s.unit }}</span>
@@ -2074,7 +2109,18 @@ watch(() => store.status?.ready, (ready, prev) => {
</Button> </Button>
</div> </div>
<div class="text-xs text-muted-foreground"> <div class="text-xs text-muted-foreground">
Kernel ProcessManager 统一管理应用启动时自动拉起崩溃自动重启 Kernel ProcessManager 统一管理崩溃自动重启
</div>
<!-- 应用启动时自动启动 toggle参照代理模块 -->
<div class="flex items-center justify-between rounded-md border p-3">
<div>
<p class="text-sm">应用启动时自动启动监控内核</p>
<p class="text-xs text-muted-foreground">软件启动时自动运行 ThingHK 内核</p>
</div>
<Switch
:model-value="store.autoStart"
@update:model-value="(v: boolean) => store.setAutoStart(v)"
/>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
@@ -2108,7 +2154,7 @@ watch(() => store.status?.ready, (ready, prev) => {
>{{ pathDisplay || pathShort }}</span> >{{ pathDisplay || pathShort }}</span>
<span v-else class="font-mono text-xs text-right cursor-default">{{ pathShort }}</span> <span v-else class="font-mono text-xs text-right cursor-default">{{ pathShort }}</span>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent class="max-w-[400px] break-all">{{ store.kernelInfo.path ?? '' }}</TooltipContent> <TooltipContent class="max-w-[480px] break-words">{{ store.kernelInfo.path ?? '' }}</TooltipContent>
</Tooltip> </Tooltip>
<template v-if="store.kernelInfo.exists"> <template v-if="store.kernelInfo.exists">
<Tooltip> <Tooltip>
+4 -1
View File
@@ -30,9 +30,12 @@ export const moduleConfig: ModuleConfig = {
}, },
lifecycle: { lifecycle: {
onEnable: async () => { onEnable: async () => {
// 启用模块时拉起 Kernel 并开始 SSE 订阅 // 若用户在监控设置中开启了"自动启动",则随模块启用而运行 Kernel
try { try {
const autoStart = await invoke<boolean>('monitor_get_auto_start')
if (autoStart) {
await invoke('monitor_start') await invoke('monitor_start')
}
} catch { } catch {
/* 忽略:可能 Kernel 未安装 */ /* 忽略:可能 Kernel 未安装 */
} }
+14 -6
View File
@@ -345,14 +345,26 @@ onMounted(() => {
// 窗口/标签页不可见时暂停状态轮询,恢复可见后下个 tick 自动继续 // 窗口/标签页不可见时暂停状态轮询,恢复可见后下个 tick 自动继续
if (document.hidden) return if (document.hidden) return
await store.refreshStatus() await store.refreshStatus()
// 同步系统代理真实状态(注册表可能被外部改动,3s 周期足够感知)
await store.refreshSystemProxy()
}, 3000) }, 3000)
// 页面重新可见时立即刷新一次系统代理状态(切回标签页/从托盘返回主窗口)
document.addEventListener('visibilitychange', onVisibilityChange)
}) })
onUnmounted(() => { onUnmounted(() => {
if (statusTimer) clearInterval(statusTimer) if (statusTimer) clearInterval(statusTimer)
document.removeEventListener('visibilitychange', onVisibilityChange)
stopAutoSwitch() stopAutoSwitch()
}) })
/** 页面可见性变化时刷新系统代理状态(低成本感知外部修改) */
async function onVisibilityChange() {
if (!document.hidden) {
await store.refreshSystemProxy()
}
}
watch(running, async (val, old) => { watch(running, async (val, old) => {
if (val && !old) { if (val && !old) {
await store.waitForApi() await store.waitForApi()
@@ -978,13 +990,9 @@ tabsStore.registerSave(saveSettingsForm)
<div class="flex items-center gap-1.5 min-w-0"> <div class="flex items-center gap-1.5 min-w-0">
<Tooltip> <Tooltip>
<TooltipTrigger as-child> <TooltipTrigger as-child>
<span <span class="font-mono text-xs text-right truncate cursor-default">{{ pathDisplay || pathShort }}</span>
v-if="store.kernel?.exists"
class="font-mono text-xs text-right truncate cursor-default"
>{{ pathDisplay || pathShort }}</span>
<span v-else class="font-mono text-xs text-right cursor-default">{{ pathShort }}</span>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent class="max-w-[400px] break-all">{{ store.kernel?.path ?? '' }}</TooltipContent> <TooltipContent class="max-w-[480px] break-words">{{ store.kernel?.path ?? '' }}</TooltipContent>
</Tooltip> </Tooltip>
<template v-if="store.kernel?.exists"> <template v-if="store.kernel?.exists">
<Tooltip> <Tooltip>
+4
View File
@@ -51,6 +51,10 @@ export const moduleConfig: ModuleConfig = {
const s = await commands.proxyGetSettings() const s = await commands.proxyGetSettings()
if (s.autoStart) { if (s.autoStart) {
await commands.proxyStart() await commands.proxyStart()
// 若同时开启了"启动时自动开启系统代理",则一并开启系统代理
if (s.autoSystemProxy) {
await commands.proxySetSystemProxy()
}
} }
} catch { } catch {
/* 忽略:可能内核未安装 */ /* 忽略:可能内核未安装 */
+17 -9
View File
@@ -6,6 +6,7 @@ import { getCurrentWindow, Effect, EffectState } from '@tauri-apps/api/window'
import { commands } from '@/lib/bindings' import { commands } from '@/lib/bindings'
import { Search, Command, Loader2, CornerDownLeft, Calculator, Globe, Lock, ChevronRight, Terminal, History, FolderOpen, Ruler, Trash2 } from '@lucide/vue' import { Search, Command, Loader2, CornerDownLeft, Calculator, Globe, Lock, ChevronRight, Terminal, History, FolderOpen, Ruler, Trash2 } from '@lucide/vue'
import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from '@/components/ui/accordion' import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from '@/components/ui/accordion'
import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'
import { aggregateSearch, loadAppIconsForResults, recordHistoryItem, getMoreHistoryItems, getMoreHistoryCount, setFileIndexReady, type QPItem, type QPSubAction } from './providers' import { aggregateSearch, loadAppIconsForResults, recordHistoryItem, getMoreHistoryItems, getMoreHistoryCount, setFileIndexReady, type QPItem, type QPSubAction } from './providers'
import { EVENTS, STORAGE_KEYS } from '@/lib/constants' import { EVENTS, STORAGE_KEYS } from '@/lib/constants'
@@ -429,9 +430,10 @@ onUnmounted(() => {
<template> <template>
<div class="qp-root flex flex-col h-screen w-screen" @keydown="onKeydown"> <div class="qp-root flex flex-col h-screen w-screen" @keydown="onKeydown">
<TooltipProvider>
<!-- 搜索输入 --> <!-- 搜索输入 -->
<div class="qp-input-wrap"> <div class="qp-input-wrap">
<Search class="h-4 w-4 text-muted-foreground shrink-0" /> <Search class="size-4 text-muted-foreground shrink-0" />
<input <input
ref="inputRef" ref="inputRef"
v-model="query" v-model="query"
@@ -446,16 +448,16 @@ onUnmounted(() => {
<!-- 结果区 --> <!-- 结果区 -->
<div class="qp-results"> <div class="qp-results">
<div v-if="loading && !hasResults()" class="qp-empty"> <div v-if="loading && !hasResults()" class="qp-empty">
<Loader2 class="h-6 w-6 animate-spin mb-2" /> <Loader2 class="size-6 animate-spin mb-2" />
<p class="text-sm">搜索中</p> <p class="text-sm">搜索中</p>
</div> </div>
<div v-else-if="!hasResults() && query.trim()" class="qp-empty"> <div v-else-if="!hasResults() && query.trim()" class="qp-empty">
<Search class="h-10 w-10 mb-3 opacity-40" /> <Search class="size-10 mb-3 opacity-40" />
<p class="text-sm">无匹配结果</p> <p class="text-sm">无匹配结果</p>
<p class="text-xs mt-1 opacity-60"> Enter 在搜索引擎中查找</p> <p class="text-xs mt-1 opacity-60"> Enter 在搜索引擎中查找</p>
</div> </div>
<div v-else-if="!hasResults()" class="qp-empty"> <div v-else-if="!hasResults()" class="qp-empty">
<Command class="h-10 w-10 mb-3 opacity-40" /> <Command class="size-10 mb-3 opacity-40" />
<p class="text-sm">输入关键词开始搜索</p> <p class="text-sm">输入关键词开始搜索</p>
<p class="text-xs mt-1 opacity-60">命令 · 计算 · 系统 · 网页</p> <p class="text-xs mt-1 opacity-60">命令 · 计算 · 系统 · 网页</p>
</div> </div>
@@ -477,7 +479,7 @@ onUnmounted(() => {
<component <component
v-else v-else
:is="groupIcon(item.group)" :is="groupIcon(item.group)"
class="h-4 w-4 text-muted-foreground shrink-0" class="size-4 text-muted-foreground shrink-0"
:class="isAppLike(item) ? '' : 'mt-0.5'" :class="isAppLike(item) ? '' : 'mt-0.5'"
/> />
<div class="flex-1 min-w-0 flex" :class="isAppLike(item) ? 'items-center' : 'flex-col'"> <div class="flex-1 min-w-0 flex" :class="isAppLike(item) ? 'items-center' : 'flex-col'">
@@ -522,7 +524,7 @@ onUnmounted(() => {
<component <component
v-else v-else
:is="groupIcon(item.group)" :is="groupIcon(item.group)"
class="h-4 w-4 text-muted-foreground shrink-0" class="size-4 text-muted-foreground shrink-0"
:class="isAppLike(item) ? '' : 'mt-0.5'" :class="isAppLike(item) ? '' : 'mt-0.5'"
/> />
<div class="flex-1 min-w-0 flex" :class="isAppLike(item) ? 'items-center' : 'flex-col'"> <div class="flex-1 min-w-0 flex" :class="isAppLike(item) ? 'items-center' : 'flex-col'">
@@ -552,7 +554,7 @@ onUnmounted(() => {
<component <component
v-else v-else
:is="groupIcon(item.group)" :is="groupIcon(item.group)"
class="h-4 w-4 text-muted-foreground shrink-0" class="size-4 text-muted-foreground shrink-0"
:class="isAppLike(item) ? '' : 'mt-0.5'" :class="isAppLike(item) ? '' : 'mt-0.5'"
/> />
<div class="flex-1 min-w-0 flex" :class="isAppLike(item) ? 'items-center' : 'flex-col'"> <div class="flex-1 min-w-0 flex" :class="isAppLike(item) ? 'items-center' : 'flex-col'">
@@ -605,10 +607,15 @@ onUnmounted(() => {
<div v-if="pendingDelete" class="qp-confirm-mask" @click.self="cancelDelete"> <div v-if="pendingDelete" class="qp-confirm-mask" @click.self="cancelDelete">
<div class="qp-confirm-box" role="alertdialog" aria-modal="true"> <div class="qp-confirm-box" role="alertdialog" aria-modal="true">
<div class="qp-confirm-head"> <div class="qp-confirm-head">
<Trash2 class="h-4 w-4 shrink-0 text-destructive" /> <Trash2 class="size-4 shrink-0 text-destructive" />
<p class="qp-confirm-title">确定删除</p> <p class="qp-confirm-title">确定删除</p>
</div> </div>
<p class="qp-confirm-name truncate" :title="pendingDelete.name">{{ pendingDelete.name }}</p> <Tooltip>
<TooltipTrigger as-child>
<p class="qp-confirm-name truncate">{{ pendingDelete.name }}</p>
</TooltipTrigger>
<TooltipContent>{{ pendingDelete.name }}</TooltipContent>
</Tooltip>
<p class="qp-confirm-tip">将移动到回收站{{ pendingDelete.isDir ? '(含所有子内容)' : '' }}删除后可在回收站中还原</p> <p class="qp-confirm-tip">将移动到回收站{{ pendingDelete.isDir ? '(含所有子内容)' : '' }}删除后可在回收站中还原</p>
<div class="flex items-center justify-end gap-2 mt-4"> <div class="flex items-center justify-end gap-2 mt-4">
<button class="qp-confirm-btn" @click="cancelDelete">取消</button> <button class="qp-confirm-btn" @click="cancelDelete">取消</button>
@@ -616,6 +623,7 @@ onUnmounted(() => {
</div> </div>
</div> </div>
</div> </div>
</TooltipProvider>
</div> </div>
</template> </template>
+13 -13
View File
@@ -276,11 +276,11 @@ async function changeEngine(v: string) {
<!-- 标题与测试 --> <!-- 标题与测试 -->
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<h2 class="text-lg font-semibold flex items-center gap-2"> <h2 class="text-lg font-semibold flex items-center gap-2">
<Command class="h-5 w-5 text-primary" /> <Command class="size-5 text-primary" />
快速面板 快速面板
</h2> </h2>
<Button size="sm" @click="testPopup"> <Button size="sm" @click="testPopup">
<Zap class="h-4 w-4 mr-1" /> <Zap class="size-4 mr-1" />
测试唤起 测试唤起
</Button> </Button>
</div> </div>
@@ -289,7 +289,7 @@ async function changeEngine(v: string) {
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle class="text-sm flex items-center gap-2"> <CardTitle class="text-sm flex items-center gap-2">
<Keyboard class="h-4 w-4" /> <Keyboard class="size-4" />
唤起快捷键 唤起快捷键
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
@@ -350,7 +350,7 @@ async function changeEngine(v: string) {
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle class="text-sm flex items-center gap-2"> <CardTitle class="text-sm flex items-center gap-2">
<Globe class="h-4 w-4" /> <Globe class="size-4" />
默认搜索引擎 默认搜索引擎
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
@@ -376,7 +376,7 @@ async function changeEngine(v: string) {
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle class="text-sm flex items-center gap-2"> <CardTitle class="text-sm flex items-center gap-2">
<FolderTree class="h-4 w-4" /> <FolderTree class="size-4" />
文件索引 文件索引
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
@@ -406,14 +406,14 @@ async function changeEngine(v: string) {
class="flex items-center gap-2 p-2 rounded-md bg-muted/40" class="flex items-center gap-2 p-2 rounded-md bg-muted/40"
> >
<FolderTree class="h-3.5 w-3.5 text-muted-foreground shrink-0" /> <FolderTree class="h-3.5 w-3.5 text-muted-foreground shrink-0" />
<span class="text-xs font-mono truncate flex-1" :title="dir">{{ dir }}</span> <span class="text-xs font-mono truncate flex-1">{{ dir }}</span>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
class="h-6 w-6 shrink-0 hover:text-destructive" class="size-6 shrink-0 hover:text-destructive"
@click="removeDir(idx)" @click="removeDir(idx)"
> >
<X class="h-3 w-3" /> <X class="size-3" />
</Button> </Button>
</div> </div>
<div v-if="!form.indexDirs.length" class="text-xs text-muted-foreground py-2"> <div v-if="!form.indexDirs.length" class="text-xs text-muted-foreground py-2">
@@ -435,7 +435,7 @@ async function changeEngine(v: string) {
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle class="text-sm flex items-center gap-2"> <CardTitle class="text-sm flex items-center gap-2">
<Terminal class="h-4 w-4" /> <Terminal class="size-4" />
自定义命令 自定义命令
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
@@ -452,11 +452,11 @@ async function changeEngine(v: string) {
<p class="text-xs font-medium truncate">{{ cmd.title }}</p> <p class="text-xs font-medium truncate">{{ cmd.title }}</p>
<p class="text-xs text-muted-foreground font-mono truncate">{{ cmd.command }} {{ cmd.args.join(' ') }}</p> <p class="text-xs text-muted-foreground font-mono truncate">{{ cmd.command }} {{ cmd.args.join(' ') }}</p>
</div> </div>
<Button variant="ghost" size="icon" class="h-6 w-6 shrink-0" @click="editCustomCommand(idx)"> <Button variant="ghost" size="icon" class="size-6 shrink-0" @click="editCustomCommand(idx)">
<Pencil class="h-3 w-3" /> <Pencil class="size-3" />
</Button> </Button>
<Button variant="ghost" size="icon" class="h-6 w-6 shrink-0 hover:text-destructive" @click="removeCustomCommand(idx)"> <Button variant="ghost" size="icon" class="size-6 shrink-0 hover:text-destructive" @click="removeCustomCommand(idx)">
<X class="h-3 w-3" /> <X class="size-3" />
</Button> </Button>
</div> </div>
<div v-if="!form.customCommands.length" class="text-xs text-muted-foreground py-2"> <div v-if="!form.customCommands.length" class="text-xs text-muted-foreground py-2">
+47 -20
View File
@@ -13,6 +13,7 @@ import {
Eraser, Undo2, Redo2, Trash2, Copy, Save, X, Image as ImageIcon, Eraser, Undo2, Redo2, Trash2, Copy, Save, X, Image as ImageIcon,
} from '@lucide/vue' } from '@lucide/vue'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'
// ===== 标注数据结构 ===== // ===== 标注数据结构 =====
type ToolType = 'rect' | 'arrow' | 'pen' | 'text' | 'mosaic' | 'highlight' type ToolType = 'rect' | 'arrow' | 'pen' | 'text' | 'mosaic' | 'highlight'
@@ -451,41 +452,48 @@ onUnmounted(() => {
<template> <template>
<div class="h-screen w-screen flex flex-col bg-zinc-950 text-zinc-100 overflow-hidden"> <div class="h-screen w-screen flex flex-col bg-zinc-950 text-zinc-100 overflow-hidden">
<TooltipProvider>
<!-- 标题栏 --> <!-- 标题栏 -->
<div <div
class="h-9 flex items-center justify-between px-3 bg-zinc-900 border-b border-zinc-800 shrink-0 select-none" class="h-9 flex items-center justify-between px-3 bg-zinc-900 border-b border-zinc-800 shrink-0 select-none"
data-tauri-drag-region data-tauri-drag-region
> >
<span class="text-sm font-medium">截图编辑器</span> <span class="text-sm font-medium">截图编辑器</span>
<Tooltip>
<TooltipTrigger as-child>
<Button <Button
variant="ghost" variant="ghost"
size="icon-sm" size="icon-sm"
class="text-zinc-400 hover:text-white hover:bg-zinc-800" class="text-zinc-400 hover:text-white hover:bg-zinc-800"
title="关闭"
@click="closeWindow" @click="closeWindow"
@mousedown.stop @mousedown.stop
> >
<X class="h-4 w-4" /> <X class="size-4" />
</Button> </Button>
</TooltipTrigger>
<TooltipContent>关闭</TooltipContent>
</Tooltip>
</div> </div>
<!-- 工具栏 --> <!-- 工具栏 -->
<div class="flex items-center gap-3 px-3 py-2 bg-zinc-900 border-b border-zinc-800 shrink-0 flex-wrap"> <div class="flex items-center gap-3 px-3 py-2 bg-zinc-900 border-b border-zinc-800 shrink-0 flex-wrap">
<!-- 工具组 --> <!-- 工具组 -->
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<Tooltip v-for="tool in TOOLS" :key="tool.value">
<TooltipTrigger as-child>
<Button <Button
v-for="tool in TOOLS"
:key="tool.value"
variant="ghost" variant="ghost"
size="icon-sm" size="icon-sm"
:class="currentTool === tool.value :class="currentTool === tool.value
? 'bg-zinc-700 text-white hover:bg-zinc-700 hover:text-white' ? 'bg-zinc-700 text-white hover:bg-zinc-700 hover:text-white'
: 'text-zinc-400 hover:bg-zinc-800 hover:text-white'" : 'text-zinc-400 hover:bg-zinc-800 hover:text-white'"
:title="tool.label"
@click="currentTool = tool.value" @click="currentTool = tool.value"
> >
<component :is="tool.icon" class="h-4 w-4" /> <component :is="tool.icon" class="size-4" />
</Button> </Button>
</TooltipTrigger>
<TooltipContent>{{ tool.label }}</TooltipContent>
</Tooltip>
</div> </div>
<div class="h-6 w-px bg-zinc-700" /> <div class="h-6 w-px bg-zinc-700" />
@@ -493,15 +501,17 @@ onUnmounted(() => {
<!-- 颜色 --> <!-- 颜色 -->
<div class="flex items-center gap-1.5"> <div class="flex items-center gap-1.5">
<span class="text-xs text-zinc-500">颜色</span> <span class="text-xs text-zinc-500">颜色</span>
<Tooltip v-for="c in COLORS" :key="c">
<TooltipTrigger as-child>
<button <button
v-for="c in COLORS"
:key="c"
class="size-6 rounded-full border border-zinc-500 transition-transform hover:scale-110" class="size-6 rounded-full border border-zinc-500 transition-transform hover:scale-110"
:class="currentColor === c ? 'ring-2 ring-blue-400 ring-offset-1 ring-offset-zinc-900 scale-110' : ''" :class="currentColor === c ? 'ring-2 ring-blue-400 ring-offset-1 ring-offset-zinc-900 scale-110' : ''"
:style="{ backgroundColor: c }" :style="{ backgroundColor: c }"
:title="c"
@click="currentColor = c" @click="currentColor = c"
/> />
</TooltipTrigger>
<TooltipContent>{{ c }}</TooltipContent>
</Tooltip>
</div> </div>
<div class="h-6 w-px bg-zinc-700" /> <div class="h-6 w-px bg-zinc-700" />
@@ -558,46 +568,62 @@ onUnmounted(() => {
<!-- 历史 / 清空 --> <!-- 历史 / 清空 -->
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<Tooltip>
<TooltipTrigger as-child>
<Button <Button
variant="ghost" variant="ghost"
size="icon-sm" size="icon-sm"
class="text-zinc-400 hover:bg-zinc-800 hover:text-white" class="text-zinc-400 hover:bg-zinc-800 hover:text-white"
:disabled="!canUndo" :disabled="!canUndo"
title="橡皮(撤销最近标注)"
@click="undo" @click="undo"
> >
<Eraser class="h-4 w-4" /> <Eraser class="size-4" />
</Button> </Button>
</TooltipTrigger>
<TooltipContent>橡皮撤销最近标注</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger as-child>
<Button <Button
variant="ghost" variant="ghost"
size="icon-sm" size="icon-sm"
class="text-zinc-400 hover:bg-zinc-800 hover:text-white" class="text-zinc-400 hover:bg-zinc-800 hover:text-white"
:disabled="!canUndo" :disabled="!canUndo"
title="撤销"
@click="undo" @click="undo"
> >
<Undo2 class="h-4 w-4" /> <Undo2 class="size-4" />
</Button> </Button>
</TooltipTrigger>
<TooltipContent>撤销</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger as-child>
<Button <Button
variant="ghost" variant="ghost"
size="icon-sm" size="icon-sm"
class="text-zinc-400 hover:bg-zinc-800 hover:text-white" class="text-zinc-400 hover:bg-zinc-800 hover:text-white"
:disabled="!canRedo" :disabled="!canRedo"
title="重做"
@click="redo" @click="redo"
> >
<Redo2 class="h-4 w-4" /> <Redo2 class="size-4" />
</Button> </Button>
</TooltipTrigger>
<TooltipContent>重做</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger as-child>
<Button <Button
variant="ghost" variant="ghost"
size="icon-sm" size="icon-sm"
class="text-zinc-400 hover:bg-red-900 hover:text-white" class="text-zinc-400 hover:bg-red-900 hover:text-white"
:disabled="!canUndo" :disabled="!canUndo"
title="清空"
@click="clearAll" @click="clearAll"
> >
<Trash2 class="h-4 w-4" /> <Trash2 class="size-4" />
</Button> </Button>
</TooltipTrigger>
<TooltipContent>清空</TooltipContent>
</Tooltip>
</div> </div>
</div> </div>
@@ -608,7 +634,7 @@ onUnmounted(() => {
v-if="loadError" v-if="loadError"
class="flex flex-col items-center justify-center h-full gap-3 text-zinc-400" class="flex flex-col items-center justify-center h-full gap-3 text-zinc-400"
> >
<ImageIcon class="h-12 w-12 opacity-40" /> <ImageIcon class="size-12 opacity-40" />
<p>未找到待编辑的截图</p> <p>未找到待编辑的截图</p>
<Button variant="ghost" class="text-zinc-200 hover:bg-zinc-800 hover:text-white" @click="closeWindow"> <Button variant="ghost" class="text-zinc-200 hover:bg-zinc-800 hover:text-white" @click="closeWindow">
关闭 关闭
@@ -655,13 +681,14 @@ onUnmounted(() => {
取消 取消
</Button> </Button>
<Button variant="ghost" class="text-zinc-200 hover:bg-zinc-800 hover:text-white" @click="saveToFile"> <Button variant="ghost" class="text-zinc-200 hover:bg-zinc-800 hover:text-white" @click="saveToFile">
<Save class="h-4 w-4" /> <Save class="size-4" />
保存到文件 保存到文件
</Button> </Button>
<Button variant="ghost" class="bg-blue-600 text-white hover:bg-blue-500" @click="copyToClipboard"> <Button variant="ghost" class="bg-blue-600 text-white hover:bg-blue-500" @click="copyToClipboard">
<Copy class="h-4 w-4" /> <Copy class="size-4" />
复制到剪贴板 复制到剪贴板
</Button> </Button>
</div> </div>
</TooltipProvider>
</div> </div>
</template> </template>
+154 -38
View File
@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onUnmounted, watch, nextTick } from 'vue' import { ref, onUnmounted, watch, nextTick } from 'vue'
import { import {
Keyboard, Settings, FolderOpen, Camera, Copy, Save, Trash2, Timer, Keyboard, Settings, FolderOpen, Camera, Copy, Save, Trash2, Timer, StickyNote,
Image as ImageIcon, Loader2, Image as ImageIcon, Loader2,
} from '@lucide/vue' } from '@lucide/vue'
import { toast } from 'vue-sonner' import { toast } from 'vue-sonner'
@@ -14,6 +14,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Switch } from '@/components/ui/switch' import { Switch } from '@/components/ui/switch'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'
const store = useScreenshotStore() const store = useScreenshotStore()
@@ -23,7 +24,7 @@ const tabsListRef = useModuleTabs(activeTab, [
{ value: 'history', label: '历史' }, { value: 'history', label: '历史' },
]) ])
const HISTORY_LIMITS = [6, 12, 24, 48] const HISTORY_LIMITS = [24, 48, 96]
const DELAY_OPTIONS = [0, 1, 2, 3, 5] const DELAY_OPTIONS = [0, 1, 2, 3, 5]
function formatTime(t: number): string { function formatTime(t: number): string {
@@ -69,14 +70,45 @@ async function handleSave(item: RecentCapture) {
} }
} }
async function handlePin(item: RecentCapture) {
const idx = store.recent.findIndex((r) => r.id === item.id)
await store.togglePin(idx >= 0 ? idx : 0)
}
function handleDelete(item: RecentCapture) { function handleDelete(item: RecentCapture) {
void store.removeRecent(item) void store.removeRecent(item)
toast.success('已从历史移除') toast.success('已从历史移除')
} }
// ===== 快捷键录入器 ===== // ===== 历史保留数量:预设 + 自定义输入 =====
const recording = ref(false) const customLimit = ref('')
const recorderRef = ref<HTMLDivElement | null>(null)
function onLimitPreset(n: number) {
store.setSettings({ historyLimit: n })
}
function onCustomLimit() {
const v = parseInt(customLimit.value, 10)
if (!Number.isFinite(v) || v < 1) {
toast.warning('请输入大于 0 的数字')
return
}
store.setSettings({ historyLimit: Math.min(999, Math.max(1, Math.floor(v))) })
}
// 当前值不在预设中时,把自定义输入框预填为当前值
watch(
() => store.settings.historyLimit,
(v) => {
if (!HISTORY_LIMITS.includes(v)) customLimit.value = String(v)
},
{ immediate: true },
)
// ===== 快捷键录入器(截图 / 贴图共用) =====
const recordingField = ref<'capture' | 'pin' | null>(null)
const captureRecorderRef = ref<HTMLDivElement | null>(null)
const pinRecorderRef = ref<HTMLDivElement | null>(null)
/** 把存储的快捷键字符串格式化为展示形式:ctrl+alt+a → Ctrl + Alt + A */ /** 把存储的快捷键字符串格式化为展示形式:ctrl+alt+a → Ctrl + Alt + A */
function displayShortcut(s: string): string { function displayShortcut(s: string): string {
@@ -119,11 +151,11 @@ function eventToShortcut(e: KeyboardEvent): string | null {
} }
function onRecordKey(e: KeyboardEvent) { function onRecordKey(e: KeyboardEvent) {
if (!recording.value) return if (!recordingField.value) return
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
if (e.key === 'Escape') { if (e.key === 'Escape') {
recording.value = false recordingField.value = null
return return
} }
// 仅修饰键按下时不结束(等待主键) // 仅修饰键按下时不结束(等待主键)
@@ -133,24 +165,27 @@ function onRecordKey(e: KeyboardEvent) {
toast.warning('不支持的按键组合,请使用字母/数字/功能键 + 修饰键') toast.warning('不支持的按键组合,请使用字母/数字/功能键 + 修饰键')
return return
} }
recording.value = false const field = recordingField.value
void commitShortcut(combo) recordingField.value = null
void commitShortcut(field, combo)
} }
async function commitShortcut(combo: string) { async function commitShortcut(field: 'capture' | 'pin', combo: string) {
const ok = await store.setShortcut(combo) const ok = field === 'capture'
? await store.setShortcut(combo)
: await store.setPinShortcut(combo)
if (ok) toast.success(`快捷键已更新为 ${displayShortcut(combo)}`) if (ok) toast.success(`快捷键已更新为 ${displayShortcut(combo)}`)
else toast.error('快捷键注册失败,可能被其他程序占用') else toast.error('快捷键注册失败,可能被其他程序占用')
} }
async function startRecord() { async function startRecord(field: 'capture' | 'pin') {
recording.value = true recordingField.value = field
await nextTick() await nextTick()
recorderRef.value?.focus() ;(field === 'capture' ? captureRecorderRef : pinRecorderRef).value?.focus()
} }
watch(recording, (on) => { watch(recordingField, (field) => {
if (on) window.addEventListener('keydown', onRecordKey, true) if (field) window.addEventListener('keydown', onRecordKey, true)
else window.removeEventListener('keydown', onRecordKey, true) else window.removeEventListener('keydown', onRecordKey, true)
}) })
@@ -159,6 +194,11 @@ async function clearShortcut() {
toast.success('已禁用截图快捷键') toast.success('已禁用截图快捷键')
} }
async function clearPinShortcut() {
await store.setPinShortcut('')
toast.success('已禁用贴图快捷键')
}
// 导出监听(screenshot-exported)由应用级注册(main.ts/App.vue),随应用生命周期管理; // 导出监听(screenshot-exported)由应用级注册(main.ts/App.vue),随应用生命周期管理;
// 模块卸载不得销毁该单例监听,否则离开截图模块后全局快捷键截图将不记录历史/不自动保存。 // 模块卸载不得销毁该单例监听,否则离开截图模块后全局快捷键截图将不记录历史/不自动保存。
@@ -188,7 +228,7 @@ onUnmounted(() => {
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle class="text-sm flex items-center gap-2"> <CardTitle class="text-sm flex items-center gap-2">
<Keyboard class="h-4 w-4 text-primary" /> <Keyboard class="size-4 text-primary" />
截图快捷键 截图快捷键
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
@@ -198,13 +238,13 @@ onUnmounted(() => {
<div class="flex items-center gap-2 flex-wrap"> <div class="flex items-center gap-2 flex-wrap">
<!-- 快捷键录入器 --> <!-- 快捷键录入器 -->
<div <div
ref="recorderRef" ref="captureRecorderRef"
class="hotkey-recorder" class="hotkey-recorder"
:class="{ recording }" :class="{ recording: recordingField === 'capture' }"
tabindex="0" tabindex="0"
@click="startRecord" @click="startRecord('capture')"
> >
<template v-if="recording">按下快捷键Esc 取消</template> <template v-if="recordingField === 'capture'">按下快捷键Esc 取消</template>
<template v-else-if="store.settings.shortcut"> <template v-else-if="store.settings.shortcut">
{{ displayShortcut(store.settings.shortcut) }} {{ displayShortcut(store.settings.shortcut) }}
</template> </template>
@@ -219,7 +259,7 @@ onUnmounted(() => {
>清除</Button> >清除</Button>
</div> </div>
<p class="text-sm text-muted-foreground"> <p class="text-sm text-muted-foreground">
点击方框录入自定义快捷键留空则禁用默认 Ctrl + Alt + A 点击方框录入自定义快捷键留空则禁用默认 Ctrl + 1
</p> </p>
</div> </div>
<Button <Button
@@ -227,8 +267,8 @@ onUnmounted(() => {
:disabled="store.capturing" :disabled="store.capturing"
@click="handleCapture" @click="handleCapture"
> >
<Loader2 v-if="store.capturing" class="h-4 w-4 animate-spin" /> <Loader2 v-if="store.capturing" class="size-4 animate-spin" />
<Camera v-else class="h-4 w-4" /> <Camera v-else class="size-4" />
立即截图 立即截图
</Button> </Button>
</div> </div>
@@ -246,11 +286,52 @@ onUnmounted(() => {
</CardContent> </CardContent>
</Card> </Card>
<!-- 贴图快捷键 -->
<Card>
<CardHeader>
<CardTitle class="text-sm flex items-center gap-2">
<StickyNote class="size-4 text-primary" />
贴图快捷键
</CardTitle>
</CardHeader>
<CardContent>
<div class="flex items-center justify-between gap-4 py-1">
<div class="space-y-1 min-w-0">
<div class="flex items-center gap-2 flex-wrap">
<div
ref="pinRecorderRef"
class="hotkey-recorder"
:class="{ recording: recordingField === 'pin' }"
tabindex="0"
@click="startRecord('pin')"
>
<template v-if="recordingField === 'pin'">按下快捷键Esc 取消</template>
<template v-else-if="store.settings.pinShortcut">
{{ displayShortcut(store.settings.pinShortcut) }}
</template>
<template v-else>未设置点击录入</template>
</div>
<Button
v-if="store.settings.pinShortcut"
variant="ghost"
size="sm"
class="h-8 text-muted-foreground"
@click="clearPinShortcut"
>清除</Button>
</div>
<p class="text-sm text-muted-foreground">
把最近截图以原分辨率贴到屏幕显示在原框选位置再次按下可关闭贴图默认 Alt + 1
</p>
</div>
</div>
</CardContent>
</Card>
<!-- 截图选项 --> <!-- 截图选项 -->
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle class="text-sm flex items-center gap-2"> <CardTitle class="text-sm flex items-center gap-2">
<Settings class="h-4 w-4 text-primary" /> <Settings class="size-4 text-primary" />
截图选项 截图选项
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
@@ -259,7 +340,7 @@ onUnmounted(() => {
<div class="flex items-center justify-between gap-3 py-2"> <div class="flex items-center justify-between gap-3 py-2">
<div class="space-y-1"> <div class="space-y-1">
<Label class="flex items-center gap-2 text-base font-medium"> <Label class="flex items-center gap-2 text-base font-medium">
<Timer class="h-4 w-4" /> <Timer class="size-4" />
延时截图 延时截图
</Label> </Label>
<p class="text-sm text-muted-foreground">按下快捷键后倒计时再截图便于切到目标画面</p> <p class="text-sm text-muted-foreground">按下快捷键后倒计时再截图便于切到目标画面</p>
@@ -302,7 +383,7 @@ onUnmounted(() => {
</p> </p>
</div> </div>
<Button variant="outline" size="sm" class="shrink-0" @click="chooseSaveDir"> <Button variant="outline" size="sm" class="shrink-0" @click="chooseSaveDir">
<FolderOpen class="h-4 w-4" /> <FolderOpen class="size-4" />
选择目录 选择目录
</Button> </Button>
</div> </div>
@@ -311,9 +392,9 @@ onUnmounted(() => {
<div class="flex items-center justify-between gap-3 py-2 border-t"> <div class="flex items-center justify-between gap-3 py-2 border-t">
<div class="space-y-1"> <div class="space-y-1">
<Label class="text-base font-medium">历史保留数量</Label> <Label class="text-base font-medium">历史保留数量</Label>
<p class="text-sm text-muted-foreground">超过上限的旧截图将自动移除</p> <p class="text-sm text-muted-foreground">截图完成后自动持久化保存到应用目录,超过上限的旧截图将自动移除</p>
</div> </div>
<div class="flex gap-1 shrink-0"> <div class="flex items-center gap-1.5 shrink-0">
<Button <Button
v-for="n in HISTORY_LIMITS" v-for="n in HISTORY_LIMITS"
:key="n" :key="n"
@@ -323,8 +404,19 @@ onUnmounted(() => {
:class="store.settings.historyLimit === n :class="store.settings.historyLimit === n
? 'bg-primary/10 text-primary hover:bg-primary/10' ? 'bg-primary/10 text-primary hover:bg-primary/10'
: 'text-muted-foreground'" : 'text-muted-foreground'"
@click="store.setSettings({ historyLimit: n })" @click="onLimitPreset(n)"
>{{ n }}</Button> >{{ n }}</Button>
<input
v-model="customLimit"
type="number"
min="1"
max="999"
class="h-8 w-16 rounded-md border border-input bg-transparent px-2 text-sm text-center outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50"
placeholder="自定义"
title="输入自定义保留数量"
@keydown.enter="onCustomLimit"
@blur="onCustomLimit"
>
</div> </div>
</div> </div>
</CardContent> </CardContent>
@@ -342,9 +434,9 @@ onUnmounted(() => {
v-if="store.recent.length === 0" v-if="store.recent.length === 0"
class="flex flex-col items-center justify-center h-64 text-muted-foreground gap-3" class="flex flex-col items-center justify-center h-64 text-muted-foreground gap-3"
> >
<ImageIcon class="h-12 w-12 opacity-40" /> <ImageIcon class="size-12 opacity-40" />
<p>暂无截图历史</p> <p>暂无截图历史</p>
<p class="text-xs">按 Ctrl + Alt + A 截图并导出后会显示在这里</p> <p class="text-xs">按 Ctrl + 1 截图并导出后会显示在这里</p>
</div> </div>
<!-- 历史网格 --> <!-- 历史网格 -->
@@ -373,31 +465,55 @@ onUnmounted(() => {
<div <div
class="absolute inset-0 flex items-center justify-center gap-2 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity" class="absolute inset-0 flex items-center justify-center gap-2 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity"
> >
<Tooltip>
<TooltipTrigger as-child>
<Button
variant="secondary"
size="icon-sm"
@click.stop="handlePin(item)"
>
<StickyNote class="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>贴到屏幕</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger as-child>
<Button <Button
variant="secondary" variant="secondary"
size="icon-sm" size="icon-sm"
title="复制到剪贴板"
@click.stop="handleCopy(item)" @click.stop="handleCopy(item)"
> >
<Copy class="h-4 w-4" /> <Copy class="size-4" />
</Button> </Button>
</TooltipTrigger>
<TooltipContent>复制到剪贴板</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger as-child>
<Button <Button
variant="secondary" variant="secondary"
size="icon-sm" size="icon-sm"
title="保存到文件"
@click.stop="handleSave(item)" @click.stop="handleSave(item)"
> >
<Save class="h-4 w-4" /> <Save class="size-4" />
</Button> </Button>
</TooltipTrigger>
<TooltipContent>保存到文件</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger as-child>
<Button <Button
variant="secondary" variant="secondary"
size="icon-sm" size="icon-sm"
class="text-destructive hover:text-destructive" class="text-destructive hover:text-destructive"
title="从历史移除"
@click.stop="handleDelete(item)" @click.stop="handleDelete(item)"
> >
<Trash2 class="h-4 w-4" /> <Trash2 class="size-4" />
</Button> </Button>
</TooltipTrigger>
<TooltipContent>从历史移除</TooltipContent>
</Tooltip>
</div> </div>
</div> </div>
</div> </div>
+81 -21
View File
@@ -11,6 +11,7 @@ import {
} from '@lucide/vue' } from '@lucide/vue'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { Slider } from '@/components/ui/slider' import { Slider } from '@/components/ui/slider'
import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'
import { import {
TOOLS, COLORS, BLOCK_SIZES, ALPHAS, HANDLES, HANDLE_HIT, DRAG_THRESHOLD, TOOLS, COLORS, BLOCK_SIZES, ALPHAS, HANDLES, HANDLE_HIT, DRAG_THRESHOLD,
type Phase, type ToolType, type Annotation, type DrawableAnnotation, type Phase, type ToolType, type Annotation, type DrawableAnnotation,
@@ -1779,7 +1780,16 @@ async function finish() {
} }
} }
if (!out) return if (!out) return
await emit(EVENTS.screenshotExported, { pngBase64: out.b64, width: out.w, height: out.h }) // 附带框选区域的屏幕逻辑坐标(DIP):窗口逻辑位置 + 选区图像坐标。
// 截图图像按虚拟屏 DIP 捕获,坐标按 DIP 传递,贴图窗口按 DIP 布局即可 1:1 还原
const spPos = selPhys.value
await emit(EVENTS.screenshotExported, {
pngBase64: out.b64,
width: out.w,
height: out.h,
posX: Math.round(winOuterX / dpr + spPos.x),
posY: Math.round(winOuterY / dpr + spPos.y),
})
} catch (e) { } catch (e) {
console.error('[screenshot] 完成失败', e) console.error('[screenshot] 完成失败', e)
} finally { } finally {
@@ -1801,7 +1811,14 @@ async function doSave() {
}) })
if (!path) return if (!path) return
await commands.screenshotSavePng(out.b64, path) await commands.screenshotSavePng(out.b64, path)
await emit(EVENTS.screenshotExported, { pngBase64: out.b64, width: out.w, height: out.h }) const spPos = selPhys.value
await emit(EVENTS.screenshotExported, {
pngBase64: out.b64,
width: out.w,
height: out.h,
posX: Math.round(winOuterX / dpr + spPos.x),
posY: Math.round(winOuterY / dpr + spPos.y),
})
await win.hide().catch(() => {}) await win.hide().catch(() => {})
} catch (e) { } catch (e) {
console.error('[screenshot] 保存失败', e) console.error('[screenshot] 保存失败', e)
@@ -1985,6 +2002,7 @@ onUnmounted(() => {
@mousemove="onMouseMove" @mousemove="onMouseMove"
@mouseup="onMouseUp" @mouseup="onMouseUp"
> >
<TooltipProvider>
<!-- 冻结的屏幕底图 --> <!-- 冻结的屏幕底图 -->
<img <img
v-if="imgSrc" v-if="imgSrc"
@@ -2094,31 +2112,37 @@ onUnmounted(() => {
<div v-if="showToolbar" ref="toolbarRef" class="toolbar" :style="toolbarPos" @mousedown.stop> <div v-if="showToolbar" ref="toolbarRef" class="toolbar" :style="toolbarPos" @mousedown.stop>
<!-- 标注工具 --> <!-- 标注工具 -->
<div class="tb-group"> <div class="tb-group">
<Tooltip v-for="t in TOOLS" :key="t.value">
<TooltipTrigger as-child>
<button <button
v-for="t in TOOLS"
:key="t.value"
class="tb-icon" class="tb-icon"
:class="{ active: phase === 'editing' && currentTool === t.value }" :class="{ active: phase === 'editing' && currentTool === t.value }"
:title="t.label"
@click="onSelectTool(t.value)" @click="onSelectTool(t.value)"
> >
<component :is="t.icon" class="tb-svg" /> <component :is="t.icon" class="tb-svg" />
</button> </button>
</TooltipTrigger>
<TooltipContent>{{ t.label }}</TooltipContent>
</Tooltip>
</div> </div>
<div class="tb-sep" /> <div class="tb-sep" />
<!-- 颜色色卡显示当前颜色点击打开选色板 --> <!-- 颜色色卡显示当前颜色点击打开选色板 -->
<Popover v-model:open="colorPickerOpen"> <Popover v-model:open="colorPickerOpen">
<Tooltip>
<TooltipTrigger as-child>
<PopoverTrigger as-child> <PopoverTrigger as-child>
<button <button
class="color-badge" class="color-badge"
:class="{ disabled: !colorEnabled }" :class="{ disabled: !colorEnabled }"
:style="{ '--swatch-color': effectiveColor }" :style="{ '--swatch-color': effectiveColor }"
:disabled="!colorEnabled" :disabled="!colorEnabled"
title="颜色"
/> />
</PopoverTrigger> </PopoverTrigger>
</TooltipTrigger>
<TooltipContent>颜色</TooltipContent>
</Tooltip>
<PopoverContent class="w-auto p-3" align="start"> <PopoverContent class="w-auto p-3" align="start">
<div class="color-popover"> <div class="color-popover">
<input <input
@@ -2128,15 +2152,17 @@ onUnmounted(() => {
@input="onCustomColorPick(($event.target as HTMLInputElement).value)" @input="onCustomColorPick(($event.target as HTMLInputElement).value)"
/> />
<div class="color-swatches"> <div class="color-swatches">
<Tooltip v-for="c in COLORS" :key="c">
<TooltipTrigger as-child>
<button <button
v-for="c in COLORS"
:key="c"
class="color-swatch-mini" class="color-swatch-mini"
:class="{ active: effectiveColor === c }" :class="{ active: effectiveColor === c }"
:style="{ backgroundColor: c }" :style="{ backgroundColor: c }"
:title="c"
@click="effectiveColor = c" @click="effectiveColor = c"
/> />
</TooltipTrigger>
<TooltipContent>{{ c }}</TooltipContent>
</Tooltip>
</div> </div>
</div> </div>
</PopoverContent> </PopoverContent>
@@ -2144,14 +2170,18 @@ onUnmounted(() => {
<!-- 粗细 badge点击打开 Slider --> <!-- 粗细 badge点击打开 Slider -->
<Popover v-model:open="lineWidthPickerOpen"> <Popover v-model:open="lineWidthPickerOpen">
<Tooltip>
<TooltipTrigger as-child>
<PopoverTrigger as-child> <PopoverTrigger as-child>
<button <button
class="width-badge" class="width-badge"
:class="{ disabled: !lineWidthEnabled }" :class="{ disabled: !lineWidthEnabled }"
:disabled="!lineWidthEnabled" :disabled="!lineWidthEnabled"
title="粗细"
>{{ effectiveLineWidth }}</button> >{{ effectiveLineWidth }}</button>
</PopoverTrigger> </PopoverTrigger>
</TooltipTrigger>
<TooltipContent>粗细</TooltipContent>
</Tooltip>
<PopoverContent class="w-auto p-3" align="start"> <PopoverContent class="w-auto p-3" align="start">
<div class="width-popover"> <div class="width-popover">
<div class="width-popover-header"> <div class="width-popover-header">
@@ -2174,43 +2204,62 @@ onUnmounted(() => {
<template v-if="currentTool === 'mosaic'"> <template v-if="currentTool === 'mosaic'">
<div class="tb-sep" /> <div class="tb-sep" />
<div class="tb-group"> <div class="tb-group">
<Tooltip v-for="b in BLOCK_SIZES" :key="b">
<TooltipTrigger as-child>
<button <button
v-for="b in BLOCK_SIZES"
:key="b"
class="tb-num" class="tb-num"
:class="{ active: blockSize === b }" :class="{ active: blockSize === b }"
title="马赛克块大小"
@click="blockSize = b" @click="blockSize = b"
>{{ b }}</button> >{{ b }}</button>
</TooltipTrigger>
<TooltipContent>马赛克块大小</TooltipContent>
</Tooltip>
</div> </div>
</template> </template>
<template v-if="currentTool === 'highlight'"> <template v-if="currentTool === 'highlight'">
<div class="tb-sep" /> <div class="tb-sep" />
<div class="tb-group"> <div class="tb-group">
<Tooltip v-for="a in ALPHAS" :key="a">
<TooltipTrigger as-child>
<button <button
v-for="a in ALPHAS"
:key="a"
class="tb-num" class="tb-num"
:class="{ active: highlightAlpha === a }" :class="{ active: highlightAlpha === a }"
title="高亮透明度"
@click="highlightAlpha = a" @click="highlightAlpha = a"
>{{ Math.round(a * 100) }}%</button> >{{ Math.round(a * 100) }}%</button>
</TooltipTrigger>
<TooltipContent>高亮透明度</TooltipContent>
</Tooltip>
</div> </div>
</template> </template>
<div class="tb-sep" /> <div class="tb-sep" />
<div class="tb-group"> <div class="tb-group">
<button class="tb-icon" :disabled="!canUndo" title="撤销" @click="undo"> <Tooltip>
<TooltipTrigger as-child>
<button class="tb-icon" :disabled="!canUndo" @click="undo">
<Undo2 class="tb-svg" /> <Undo2 class="tb-svg" />
</button> </button>
<button class="tb-icon" :disabled="!canRedo" title="重做" @click="redo"> </TooltipTrigger>
<TooltipContent>撤销</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger as-child>
<button class="tb-icon" :disabled="!canRedo" @click="redo">
<Redo2 class="tb-svg" /> <Redo2 class="tb-svg" />
</button> </button>
<button class="tb-icon" :disabled="!canUndo" title="清空标注" @click="clearAnnotations"> </TooltipTrigger>
<TooltipContent>重做</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger as-child>
<button class="tb-icon" :disabled="!canUndo" @click="clearAnnotations">
<Eraser class="tb-svg" /> <Eraser class="tb-svg" />
</button> </button>
</TooltipTrigger>
<TooltipContent>清空标注</TooltipContent>
</Tooltip>
</div> </div>
<div class="tb-sep" /> <div class="tb-sep" />
@@ -2221,12 +2270,22 @@ onUnmounted(() => {
<!-- 复制 / 保存(置末) --> <!-- 复制 / 保存(置末) -->
<div class="tb-group"> <div class="tb-group">
<button class="tb-icon" title="保存到文件" @click="doSave"> <Tooltip>
<TooltipTrigger as-child>
<button class="tb-icon" @click="doSave">
<Save class="tb-svg" /> <Save class="tb-svg" />
</button> </button>
<button class="tb-icon primary" title="复制到剪贴板 (Enter)" @click="finish"> </TooltipTrigger>
<TooltipContent>保存到文件</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger as-child>
<button class="tb-icon primary" @click="finish">
<Copy class="tb-svg" /> <Copy class="tb-svg" />
</button> </button>
</TooltipTrigger>
<TooltipContent>复制到剪贴板 (Enter)</TooltipContent>
</Tooltip>
</div> </div>
</div> </div>
@@ -2245,6 +2304,7 @@ onUnmounted(() => {
<div class="mag-hint">{{ hint.split(' · ').join('\n') }}</div> <div class="mag-hint">{{ hint.split(' · ').join('\n') }}</div>
</div> </div>
</div> </div>
</TooltipProvider>
</div> </div>
</template> </template>
+347
View File
@@ -0,0 +1,347 @@
<script setup lang="ts">
// 贴图窗口:无边框、透明、置顶。
// 图片以原分辨率显示,窗口定位到截图时的框选位置(原框选位置)。
// 右下角展开图标,hover 展开 上一张 / 下一张 / 关闭。
import { ref, onMounted, onUnmounted } from 'vue'
import { getCurrentWindow, currentMonitor } from '@tauri-apps/api/window'
import { LogicalPosition, LogicalSize } from '@tauri-apps/api/dpi'
import { emit, listen, type UnlistenFn } from '@tauri-apps/api/event'
import { ChevronLeft, ChevronRight, X, Maximize2, Loader2 } from '@lucide/vue'
import { EVENTS, STORAGE_KEYS } from '@/lib/constants'
// Rust 端通过 tauri-specta 生成的命令绑定(bindings.ts
import { commands } from '@/lib/bindings'
/** 图片四周留白(CSS 像素 = 逻辑像素 DIP,与截图图像分辨率单位一致),用于呈现蓝色光晕 */
const GLOW_PAD = 18
const win = getCurrentWindow()
const imgSrc = ref('')
const imgCssW = ref(0)
const imgCssH = ref(0)
const loading = ref(false)
const errorMsg = ref('')
/** 历史列表(localStorage 与主窗口共享,最新在前) */
const items = ref<{ filePath: string; width: number; height: number; posX?: number; posY?: number }[]>([])
const cur = ref(0)
let showUnlisten: UnlistenFn | null = null
/** 同步主界面主题(独立窗口无 pinia,从 localStorage 读取应用设置) */
function applyTheme() {
const root = document.documentElement
let theme = 'system'
try {
const raw = localStorage.getItem(STORAGE_KEYS.appSettings)
if (raw) {
const s = JSON.parse(raw)
theme = s.theme ?? 'system'
}
} catch { /* 忽略 */ }
let isDark: boolean
if (theme === 'system') {
isDark = window.matchMedia('(prefers-color-scheme: dark)').matches
} else {
isDark = theme === 'dark'
}
root.classList.toggle('dark', isDark)
}
/** 从 localStorage 读取主窗口持久化的截图历史(图片文件在应用数据目录) */
function readHistory() {
try {
const raw = localStorage.getItem(STORAGE_KEYS.screenshotHistory)
if (!raw) return []
const parsed = JSON.parse(raw) as {
version?: number
items?: { filePath: string; width: number; height: number; posX?: number; posY?: number }[]
}
if (!Array.isArray(parsed.items)) return []
return parsed.items
} catch {
return []
}
}
/**
* 窗口排版到目标位置(全部使用逻辑坐标/DIP):
* 截图图像的分辨率是逻辑像素(BMP 按虚拟屏 DIP 捕获),CSS 像素与 DIP 一致,
* 故图片以 item.width/height 的 CSS 尺寸显示即 1:1 还原;窗口尺寸 = 图片 + 2×留白,
* 窗口位置 = 原框选位置 - 留白,使图片精确覆盖在截图时的区域上(跨 DPI 显示器也无需换算)。
*/
async function layoutTo(item: { width: number; height: number; posX?: number; posY?: number }) {
const pad = GLOW_PAD
let x = item.posX !== undefined ? item.posX - pad : 0
let y = item.posY !== undefined ? item.posY - pad : 0
// 无定位信息(如编辑器导出)时放到当前鼠标位置附近(鼠标坐标为物理像素,需换算为 DIP)
if (item.posX === undefined || item.posY === undefined) {
try {
const [mx, my] = await commands.screenshotCursorPos()
const mon = await currentMonitor()
const s = mon?.scaleFactor || 1
x = mx / s - pad
y = my / s - pad
} catch { /* 忽略,沿用 0,0 */ }
}
// 粗略限制在鼠标所在显示器内(避免贴图完全出现在屏幕外,至少保留 40px 可见)
try {
const mon = await currentMonitor()
if (mon) {
const s = mon.scaleFactor || 1
const mlx = mon.position.x / s
const mly = mon.position.y / s
const mlw = mon.size.width / s
const mlh = mon.size.height / s
x = Math.min(Math.max(x, mlx), mlx + mlw - 40)
y = Math.min(Math.max(y, mly), mly + mlh - 40)
}
} catch { /* 忽略 */ }
// Windows 上非 resizable 窗口 setSize 会失效,须先临时开启再恢复:
// 布局结束后恢复为不可缩放,禁用边缘拖动修改大小(仅保留左键自由拖动)
await win.setPosition(new LogicalPosition(Math.round(x), Math.round(y)))
await win.setResizable(true)
await win.setSize(new LogicalSize(item.width + pad * 2, item.height + pad * 2))
await win.setResizable(false)
}
/** 加载并显示第 index 张截图(index 缺省/越界时按最近一张) */
async function loadImage(index: number) {
const list = items.value
if (!list.length) {
errorMsg.value = '暂无历史截图'
loading.value = false
await win.show().catch(() => {})
return
}
const idx = Math.max(0, Math.min(index, list.length - 1))
const item = list[idx]
cur.value = idx
loading.value = true
errorMsg.value = ''
try {
const b64 = await commands.screenshotLoadCache(item.filePath)
const dataUrl = `data:image/png;base64,${b64}`
// 预解码:等待图片就绪后再排版显示,避免闪烁
await new Promise<void>((resolve, reject) => {
const img = new Image()
img.onload = () => resolve()
img.onerror = () => reject(new Error('图片解码失败'))
img.src = dataUrl
})
// CSS 尺寸 = 原分辨率(逻辑像素/DIP),与 CSS 像素一致,1:1 还原截图时覆盖的区域
await layoutTo(item)
imgCssW.value = item.width
imgCssH.value = item.height
imgSrc.value = dataUrl
await win.show().catch(() => {})
loading.value = false
} catch (e) {
console.error('[screenshot] 贴图加载失败', e)
errorMsg.value = '加载图片失败:' + (e as Error).message
loading.value = false
await win.show().catch(() => {})
}
}
function prev() {
if (cur.value >= items.value.length - 1) return
void loadImage(cur.value + 1)
}
function next() {
if (cur.value <= 0) return
void loadImage(cur.value - 1)
}
function closePin() {
void win.hide().catch(() => {})
}
/** 左键按下时启动原生窗口拖动(与 OSD 窗口同款方案:
* 透明 + focus:false 窗口上 data-tauri-drag-region 不生效,需显式 startDragging */
function onImageMouseDown(e: MouseEvent) {
if (e.button !== 0) return
e.preventDefault()
void win.startDragging().catch(() => {})
}
/** 响应主窗口 'screenshot-pin-show':读取要贴的历史索引并加载显示 */
function openPin() {
items.value = readHistory()
let index = 0
try {
index = Number(localStorage.getItem(STORAGE_KEYS.screenshotPinIndex) ?? 0) || 0
} catch { /* 忽略 */ }
void loadImage(index)
}
onMounted(async () => {
applyTheme()
// 先注册 show 监听再通知 store 就绪,避免首轮事件丢失(与覆盖层同模式)
showUnlisten = await listen(EVENTS.screenshotPinShow, () => {
openPin()
})
await emit(EVENTS.screenshotPinReady)
})
onUnmounted(() => {
showUnlisten?.()
})
</script>
<template>
<div class="pin-root">
<!-- 图片区左键按住自由拖动显式 startDragging蓝色光晕阴影 -->
<div
class="pin-image"
:style="{ width: imgCssW + 'px', height: imgCssH + 'px' }"
@mousedown="onImageMouseDown"
>
<img
v-if="imgSrc"
:src="imgSrc"
:style="{ width: imgCssW + 'px', height: imgCssH + 'px' }"
draggable="false"
alt="贴图"
>
<div v-else-if="errorMsg" class="pin-tip">{{ errorMsg }}</div>
<div v-else-if="loading" class="pin-tip"><Loader2 class="size-5 animate-spin" /></div>
</div>
<!-- 右下角控制展开图标hover 展开 上一张 / 下一张 / 关闭 -->
<div class="pin-controls">
<div class="pin-actions">
<button
class="pin-btn"
title="上一张"
:disabled="cur >= items.length - 1"
@click="prev"
>
<ChevronLeft class="size-4" />
</button>
<button
class="pin-btn"
title="下一张"
:disabled="cur <= 0"
@click="next"
>
<ChevronRight class="size-4" />
</button>
<button class="pin-btn pin-btn-danger" title="关闭" @click="closePin">
<X class="size-4" />
</button>
</div>
<button class="pin-expand" title="更多操作">
<Maximize2 class="size-4" />
</button>
</div>
</div>
</template>
<style scoped>
.pin-root {
position: relative;
width: 100%;
height: 100%;
box-sizing: border-box;
padding: 18px; /* GLOW_PAD,与脚本常量一致;留白大于光晕扩散范围,避免光晕照出窗口外边框 */
background: transparent;
overflow: hidden;
}
.pin-image {
position: relative;
overflow: hidden;
border-radius: 0; /* 直角 */
cursor: grab;
/* 散发蓝色光晕的阴影:无实线边框,且扩散范围控制在留白内(光晕小、柔和) */
box-shadow:
0 0 6px rgba(59, 130, 246, 0.55),
0 0 14px rgba(59, 130, 246, 0.3);
}
.pin-image:active {
cursor: grabbing;
}
.pin-image img {
display: block;
user-select: none;
-webkit-user-drag: none;
}
.pin-tip {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
width: 100%;
height: 100%;
padding: 0 16px;
box-sizing: border-box;
color: rgba(255, 255, 255, 0.9);
font-size: 13px;
text-align: center;
background: rgba(15, 23, 42, 0.85);
}
/* 右下角控制:独立于图片拖动区(兄弟节点),点击不触发拖动 */
.pin-controls {
position: absolute;
right: 26px;
bottom: 26px;
z-index: 10;
display: flex;
align-items: center;
gap: 6px;
}
.pin-expand,
.pin-btn {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border: none;
border-radius: 8px;
cursor: pointer;
color: rgba(255, 255, 255, 0.92);
background: rgba(15, 23, 42, 0.65);
backdrop-filter: blur(4px);
transition: background-color 0.15s ease;
}
.pin-btn:hover {
background: rgba(30, 58, 138, 0.85);
}
.pin-btn-danger:hover {
background: rgba(185, 28, 28, 0.85);
}
.pin-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.pin-actions {
display: flex;
align-items: center;
gap: 6px;
opacity: 0;
visibility: hidden;
transform: translateX(8px);
transition:
opacity 0.15s ease,
transform 0.15s ease,
visibility 0.15s;
}
/* hover 展开三个图标按钮 */
.pin-controls:hover .pin-actions,
.pin-controls:focus-within .pin-actions {
opacity: 1;
visibility: visible;
transform: translateX(0);
}
</style>
+11 -9
View File
@@ -29,6 +29,8 @@ onMounted(() => {
searchStore.registerAction('settings', 4, () => appStore.setEffect('mica')) searchStore.registerAction('settings', 4, () => appStore.setEffect('mica'))
searchStore.registerAction('settings', 5, () => appStore.setEffect('acrylic')) searchStore.registerAction('settings', 5, () => appStore.setEffect('acrylic'))
searchStore.registerAction('settings', 6, () => appStore.toggleAutoStart()) searchStore.registerAction('settings', 6, () => appStore.toggleAutoStart())
// 主动刷新所有进程状态,确保内核 badge 显示当前真实状态(而非过期缓存)
processStore.refreshAll().catch(() => { /* 忽略:后端可能未就绪 */ })
}) })
const themes: Array<{ id: Theme; name: string; color: string; icon: typeof Sun }> = [ const themes: Array<{ id: Theme; name: string; color: string; icon: typeof Sun }> = [
@@ -132,7 +134,7 @@ const onDragEnd = () => {
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle class="flex items-center gap-2"> <CardTitle class="flex items-center gap-2">
<Sparkles class="h-5 w-5 text-primary" /> <Sparkles class="size-5 text-primary" />
常规设置 常规设置
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
@@ -153,7 +155,7 @@ const onDragEnd = () => {
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle class="flex items-center gap-2"> <CardTitle class="flex items-center gap-2">
<Layers class="h-5 w-5 text-primary" /> <Layers class="size-5 text-primary" />
主题切换 主题切换
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
@@ -175,7 +177,7 @@ const onDragEnd = () => {
> >
<component <component
:is="theme.icon" :is="theme.icon"
class="h-8 w-8 transition-colors duration-300" class="size-8 transition-colors duration-300"
:class="theme.id === 'dark' || (theme.id === 'system' && systemDark) ? 'text-white' : 'text-gray-800'" :class="theme.id === 'dark' || (theme.id === 'system' && systemDark) ? 'text-white' : 'text-gray-800'"
/> />
</div> </div>
@@ -198,7 +200,7 @@ const onDragEnd = () => {
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle class="flex items-center gap-2"> <CardTitle class="flex items-center gap-2">
<Sparkles class="h-5 w-5 text-primary" /> <Sparkles class="size-5 text-primary" />
效果切换 效果切换
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
@@ -244,7 +246,7 @@ const onDragEnd = () => {
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle class="flex items-center gap-2"> <CardTitle class="flex items-center gap-2">
<Package class="h-5 w-5 text-primary" /> <Package class="size-5 text-primary" />
模块管理 模块管理
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
@@ -271,7 +273,7 @@ const onDragEnd = () => {
<div <div
class="drag-handle cursor-grab active:cursor-grabbing text-muted-foreground/40 hover:text-muted-foreground transition-colors" class="drag-handle cursor-grab active:cursor-grabbing text-muted-foreground/40 hover:text-muted-foreground transition-colors"
> >
<GripVertical class="h-4 w-4 no-native-drag" /> <GripVertical class="size-4 no-native-drag" />
</div> </div>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>拖拽排序</TooltipContent> <TooltipContent>拖拽排序</TooltipContent>
@@ -280,7 +282,7 @@ const onDragEnd = () => {
class="w-9 h-9 rounded-lg flex items-center justify-center" class="w-9 h-9 rounded-lg flex items-center justify-center"
:class="module.enabled ? 'bg-primary/10 text-primary' : 'bg-muted text-muted-foreground'" :class="module.enabled ? 'bg-primary/10 text-primary' : 'bg-muted text-muted-foreground'"
> >
<component :is="getModuleIcon(module.icon)" class="h-5 w-5" /> <component :is="getModuleIcon(module.icon)" class="size-5" />
</div> </div>
<div> <div>
<div class="font-medium text-sm flex items-center gap-2"> <div class="font-medium text-sm flex items-center gap-2">
@@ -314,7 +316,7 @@ const onDragEnd = () => {
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle class="flex items-center gap-2"> <CardTitle class="flex items-center gap-2">
<LogOut class="h-5 w-5 text-destructive" /> <LogOut class="size-5 text-destructive" />
退出程序 退出程序
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
@@ -324,7 +326,7 @@ const onDragEnd = () => {
class="text-destructive border-destructive/20 hover:bg-destructive/10" class="text-destructive border-destructive/20 hover:bg-destructive/10"
@click="quitApp" @click="quitApp"
> >
<LogOut class="h-4 w-4 mr-2" /> <LogOut class="size-4 mr-2" />
彻底退出 彻底退出
</Button> </Button>
</CardContent> </CardContent>
+11 -11
View File
@@ -433,7 +433,7 @@ onUnmounted(() => {
<div class="tray-menu-scroll flex-1 min-h-0 overflow-y-auto"> <div class="tray-menu-scroll flex-1 min-h-0 overflow-y-auto">
<div class="tray-section"> <div class="tray-section">
<div class="tray-section-header"> <div class="tray-section-header">
<Globe class="h-4 w-4 text-primary" /> <Globe class="size-4 text-primary" />
<span class="tray-section-title">代理</span> <span class="tray-section-title">代理</span>
<span v-if="state.proxyRunning" class="tray-status-dot running"></span> <span v-if="state.proxyRunning" class="tray-status-dot running"></span>
<span v-else class="tray-status-dot stopped"></span> <span v-else class="tray-status-dot stopped"></span>
@@ -441,12 +441,12 @@ onUnmounted(() => {
<div class="tray-section-body"> <div class="tray-section-body">
<button class="tray-item" :disabled="proxyLoading" @click="handleProxyToggle"> <button class="tray-item" :disabled="proxyLoading" @click="handleProxyToggle">
<component :is="proxyButtonIcon" class="h-4 w-4" /> <component :is="proxyButtonIcon" class="size-4" />
<span>{{ proxyButtonLabel }}</span> <span>{{ proxyButtonLabel }}</span>
</button> </button>
<button v-if="state.proxyRunning" class="tray-item" :disabled="proxyLoading" @click="handleSystemProxyToggle"> <button v-if="state.proxyRunning" class="tray-item" :disabled="proxyLoading" @click="handleSystemProxyToggle">
<Power class="h-4 w-4" /> <Power class="size-4" />
<span>系统代理</span> <span>系统代理</span>
<span class="tray-toggle-indicator" :class="{ active: state.systemProxy }"></span> <span class="tray-toggle-indicator" :class="{ active: state.systemProxy }"></span>
</button> </button>
@@ -457,7 +457,7 @@ onUnmounted(() => {
:disabled="proxyLoading" :disabled="proxyLoading"
@click="handleAction('proxy_refresh')" @click="handleAction('proxy_refresh')"
> >
<RefreshCw class="h-4 w-4" :class="{ 'animate-spin': refreshing }" /> <RefreshCw class="size-4" :class="{ 'animate-spin': refreshing }" />
<span>刷新节点列表</span> <span>刷新节点列表</span>
</button> </button>
@@ -482,7 +482,7 @@ onUnmounted(() => {
:value="node.name" :value="node.name"
class="tray-select-option" class="tray-select-option"
> >
<Check v-if="node.isCurrent" class="h-3 w-3 text-primary flex-shrink-0" /> <Check v-if="node.isCurrent" class="size-3 text-primary flex-shrink-0" />
<span class="tray-select-badge-option">{{ abbreviateNodeName(node.name) }}</span> <span class="tray-select-badge-option">{{ abbreviateNodeName(node.name) }}</span>
<span class="tray-select-delay" :class="delayClass(node.delay)"> <span class="tray-select-delay" :class="delayClass(node.delay)">
{{ delayText(node.delay) }} {{ delayText(node.delay) }}
@@ -498,7 +498,7 @@ onUnmounted(() => {
<div class="tray-section"> <div class="tray-section">
<div class="tray-section-header"> <div class="tray-section-header">
<Monitor class="h-4 w-4 text-primary" /> <Monitor class="size-4 text-primary" />
<span class="tray-section-title">监控</span> <span class="tray-section-title">监控</span>
<span v-if="state.monitorRunning" class="tray-status-dot running"></span> <span v-if="state.monitorRunning" class="tray-status-dot running"></span>
<span v-else class="tray-status-dot stopped"></span> <span v-else class="tray-status-dot stopped"></span>
@@ -506,7 +506,7 @@ onUnmounted(() => {
<div class="tray-section-body"> <div class="tray-section-body">
<button class="tray-item" @click="handleAction('osd_toggle')"> <button class="tray-item" @click="handleAction('osd_toggle')">
<Monitor class="h-4 w-4" /> <Monitor class="size-4" />
<span>OSD 显示</span> <span>OSD 显示</span>
<span class="tray-toggle-indicator" :class="{ active: osdVisible }"></span> <span class="tray-toggle-indicator" :class="{ active: osdVisible }"></span>
</button> </button>
@@ -515,7 +515,7 @@ onUnmounted(() => {
:disabled="!state.monitorRunning" :disabled="!state.monitorRunning"
@click="handleAction('kernel_restart')" @click="handleAction('kernel_restart')"
> >
<RefreshCw class="h-4 w-4" /> <RefreshCw class="size-4" />
<span>重启 Kernel</span> <span>重启 Kernel</span>
</button> </button>
</div> </div>
@@ -524,18 +524,18 @@ onUnmounted(() => {
<div class="tray-separator"></div> <div class="tray-separator"></div>
<button class="tray-item" @click="handleAction('download_new')"> <button class="tray-item" @click="handleAction('download_new')">
<Download class="h-4 w-4" /> <Download class="size-4" />
<span>新建下载</span> <span>新建下载</span>
</button> </button>
<div class="tray-separator"></div> <div class="tray-separator"></div>
<button class="tray-item" @click="handleAction('settings')"> <button class="tray-item" @click="handleAction('settings')">
<Settings class="h-4 w-4" /> <Settings class="size-4" />
<span>常规设置</span> <span>常规设置</span>
</button> </button>
<button class="tray-item tray-quit" @click="handleAction('quit')"> <button class="tray-item tray-quit" @click="handleAction('quit')">
<LogOut class="h-4 w-4" /> <LogOut class="size-4" />
<span>退出</span> <span>退出</span>
</button> </button>
</div> </div>
+33 -1
View File
@@ -325,6 +325,9 @@ export const useMonitorStore = defineStore('monitor', () => {
/** 提权标志是否已启用(后续启动自动触发 UAC) */ /** 提权标志是否已启用(后续启动自动触发 UAC) */
const elevateOnLaunch = ref(false) const elevateOnLaunch = ref(false)
/** "应用启动时自动启动监控内核"开关(持久化于后端 settings.json */
const autoStart = ref(false)
/** 硬件监控配置(可用硬件 + 传感器类型清单 + 当前启用状态) */ /** 硬件监控配置(可用硬件 + 传感器类型清单 + 当前启用状态) */
const hardwareConfig = ref<HardwareConfigResponse | null>(null) const hardwareConfig = ref<HardwareConfigResponse | null>(null)
@@ -540,6 +543,27 @@ export const useMonitorStore = defineStore('monitor', () => {
} }
} }
/** 刷新"自动启动监控内核"开关状态 */
async function refreshAutoStart() {
try {
autoStart.value = await invoke<boolean>('monitor_get_auto_start')
} catch (e) {
logger.error('获取自动启动开关失败: ' + e)
}
return autoStart.value
}
/** 设置"自动启动监控内核"开关 */
async function setAutoStart(enabled: boolean) {
try {
await invoke('monitor_set_auto_start', { enabled })
autoStart.value = enabled
} catch (e) {
errorMsg.value = String(e)
logger.error('设置自动启动开关失败: ' + e)
}
}
/** 拉取硬件监控配置(可用硬件 + 传感器类型清单 + 当前启用状态) */ /** 拉取硬件监控配置(可用硬件 + 传感器类型清单 + 当前启用状态) */
async function fetchHardwareConfig() { async function fetchHardwareConfig() {
try { try {
@@ -641,7 +665,7 @@ export const useMonitorStore = defineStore('monitor', () => {
try { try {
// 启动心跳,驱动 connState 的断线判定(SSE 断开后依赖不再变化,须有心跳触发重算) // 启动心跳,驱动 connState 的断线判定(SSE 断开后依赖不再变化,须有心跳触发重算)
startHeartbeat() startHeartbeat()
await Promise.all([refreshStatus(), refreshKernelInfo(), refreshElevateOnLaunch()]) await Promise.all([refreshStatus(), refreshKernelInfo(), refreshElevateOnLaunch(), refreshAutoStart()])
await subscribe() await subscribe()
// 若 Kernel 已就绪,立即拉一次快照避免 UI 空白 // 若 Kernel 已就绪,立即拉一次快照避免 UI 空白
if (status.value?.ready) { if (status.value?.ready) {
@@ -978,6 +1002,11 @@ export const useMonitorStore = defineStore('monitor', () => {
// OSD 开关变化时创建/隐藏悬浮窗 // OSD 开关变化时创建/隐藏悬浮窗
osdWatchStops.push(watch(() => osdConfig.value.overlayEnabled, (enabled) => { osdWatchStops.push(watch(() => osdConfig.value.overlayEnabled, (enabled) => {
if (enabled) { if (enabled) {
// OSD 显示开启时自动开启"应用启动时自动启动监控内核",
// 使 OSD 持续显示不因重启而中断
if (!autoStart.value) {
setAutoStart(true).catch(e => logger.error('[OSD] 自动开启 autoStart 失败: ' + e))
}
// 开启时若显示项为空则不创建窗口 // 开启时若显示项为空则不创建窗口
if (osdConfig.value.overlayItems.length === 0) return if (osdConfig.value.overlayItems.length === 0) return
ensureOverlayWindow().catch(e => logger.error('[OSD] 创建悬浮窗失败: ' + e)) ensureOverlayWindow().catch(e => logger.error('[OSD] 创建悬浮窗失败: ' + e))
@@ -1061,6 +1090,7 @@ export const useMonitorStore = defineStore('monitor', () => {
initialized, initialized,
networkSpeed, networkSpeed,
elevateOnLaunch, elevateOnLaunch,
autoStart,
hardwareConfig, hardwareConfig,
// getters // getters
connState, connState,
@@ -1071,6 +1101,8 @@ export const useMonitorStore = defineStore('monitor', () => {
refreshStatus, refreshStatus,
refreshKernelInfo, refreshKernelInfo,
refreshElevateOnLaunch, refreshElevateOnLaunch,
refreshAutoStart,
setAutoStart,
start, start,
elevateSelf, elevateSelf,
cancelElevation, cancelElevation,
+9
View File
@@ -41,6 +41,14 @@ export const useProcessStore = defineStore('process', () => {
}) })
} }
/** 释放进程状态事件监听(应用退出或热重载时调用) */
const destroyListener = () => {
if (unlistenFn) {
unlistenFn()
unlistenFn = null
}
}
/** 通过模块 ID 启动进程(自动从注册表读取进程配置) */ /** 通过模块 ID 启动进程(自动从注册表读取进程配置) */
const startByModule = async (moduleId: string): Promise<ProcessInfo> => { const startByModule = async (moduleId: string): Promise<ProcessInfo> => {
const config = moduleRegistry.getConfig(moduleId) const config = moduleRegistry.getConfig(moduleId)
@@ -104,6 +112,7 @@ export const useProcessStore = defineStore('process', () => {
return { return {
processes, processes,
initListener, initListener,
destroyListener,
startByModule, startByModule,
stopByModule, stopByModule,
refreshStatus, refreshStatus,
+10
View File
@@ -191,6 +191,15 @@ export const useProxyStore = defineStore('proxy', () => {
return settings.value return settings.value
} }
/** 刷新系统代理真实状态(读取 Windows 注册表,不依赖 settings.json 缓存) */
const refreshSystemProxy = async () => {
try {
systemProxy.value = await commands.proxyGetSystemProxy()
} catch {
/* 忽略:后端可能未就绪 */
}
}
const saveSettings = async (s: FullProxySettings) => { const saveSettings = async (s: FullProxySettings) => {
await commands.proxySaveSettings(s) await commands.proxySaveSettings(s)
settings.value = s settings.value = s
@@ -342,6 +351,7 @@ export const useProxyStore = defineStore('proxy', () => {
// kernel & process // kernel & process
refreshKernel, refreshKernel,
refreshStatus, refreshStatus,
refreshSystemProxy,
start, start,
stop, stop,
restart, restart,
+254 -13
View File
@@ -5,7 +5,7 @@ import { PhysicalPosition, PhysicalSize } from '@tauri-apps/api/dpi'
import { availableMonitors } from '@tauri-apps/api/window' import { availableMonitors } from '@tauri-apps/api/window'
import { emit, listen, type UnlistenFn } from '@tauri-apps/api/event' import { emit, listen, type UnlistenFn } from '@tauri-apps/api/event'
import { toast } from 'vue-sonner' import { toast } from 'vue-sonner'
import { EVENTS, WINDOWS } from '@/lib/constants' import { EVENTS, STORAGE_KEYS, WINDOWS } from '@/lib/constants'
// Rust 端通过 tauri-specta 生成的命令绑定(bindings.ts // Rust 端通过 tauri-specta 生成的命令绑定(bindings.ts
import { commands } from '@/lib/bindings' import { commands } from '@/lib/bindings'
@@ -19,6 +19,9 @@ export interface RecentCapture {
height: number height: number
time: number time: number
mode: string mode: string
/** 截图时框选区域的物理屏幕坐标(贴图窗口按原位置显示;编辑器导出可能缺失) */
posX?: number
posY?: number
} }
/** 截图设置(localStorage 持久化) */ /** 截图设置(localStorage 持久化) */
@@ -31,16 +34,21 @@ export interface ScreenshotSettings {
historyLimit: number historyLimit: number
/** 全局截图快捷键(空字符串表示禁用) */ /** 全局截图快捷键(空字符串表示禁用) */
shortcut: string shortcut: string
/** 贴图全局快捷键(空字符串表示禁用),再次按下关闭贴图 */
pinShortcut: string
/** 截图延时(秒),0 表示立即截图 */ /** 截图延时(秒),0 表示立即截图 */
delay: number delay: number
} }
const SETTINGS_KEY = 'screenshot-settings' const SETTINGS_KEY = 'screenshot-settings'
/** 设置版本:结构变更(默认值/字段增删)时递增,清除旧数据以应用新默认值 */
const SETTINGS_VERSION = 2
const DEFAULT_SETTINGS: ScreenshotSettings = { const DEFAULT_SETTINGS: ScreenshotSettings = {
autoSave: false, autoSave: false,
saveDir: '', saveDir: '',
historyLimit: 12, historyLimit: 48,
shortcut: 'ctrl+alt+a', shortcut: 'ctrl+1',
pinShortcut: 'alt+1',
delay: 0, delay: 0,
} }
@@ -51,6 +59,8 @@ export const useScreenshotStore = defineStore('screenshot', () => {
let exportUnlisten: UnlistenFn | null = null let exportUnlisten: UnlistenFn | null = null
let shortcutUnlisten: UnlistenFn | null = null let shortcutUnlisten: UnlistenFn | null = null
/** 覆盖层就绪事件监听(initOverlay 注册,destroyExportListener 一并释放) */
let overlayReadyUnlisten: UnlistenFn | null = null
/** 常驻截图覆盖层窗口(启动时创建,之后每次截图复用,避免重复 WebView 初始化) */ /** 常驻截图覆盖层窗口(启动时创建,之后每次截图复用,避免重复 WebView 初始化) */
const OVERLAY_LABEL = WINDOWS.screenshotOverlay const OVERLAY_LABEL = WINDOWS.screenshotOverlay
let overlayWin: WebviewWindow | null = null let overlayWin: WebviewWindow | null = null
@@ -58,14 +68,36 @@ export const useScreenshotStore = defineStore('screenshot', () => {
let overlayReadyPromise: Promise<void> | null = null let overlayReadyPromise: Promise<void> | null = null
let readyListenerInit = false let readyListenerInit = false
// ===== 贴图窗口 =====
const PIN_LABEL = WINDOWS.screenshotPin
const PIN_INDEX_KEY = STORAGE_KEYS.screenshotPinIndex
/** 常驻贴图窗口(首次贴图时创建,之后复用;无边框透明置顶) */
let pinWin: WebviewWindow | null = null
let pinReadyUnlisten: UnlistenFn | null = null
let pinReadyListenerInit = false
let pinReadyResolve: (() => void) | null = null
let pinReadyPromise: Promise<void> | null = null
/** 贴图全局快捷键事件监听 */
let pinShortcutUnlisten: UnlistenFn | null = null
// ===== 历史持久化 =====
const HISTORY_KEY = STORAGE_KEYS.screenshotHistory
const HISTORY_VERSION = 1
// ===== 设置持久化 ===== // ===== 设置持久化 =====
function loadSettings() { function loadSettings() {
try { try {
const raw = localStorage.getItem(SETTINGS_KEY) const raw = localStorage.getItem(SETTINGS_KEY)
if (raw) { if (raw) {
const parsed = JSON.parse(raw) as Partial<ScreenshotSettings> const parsed = JSON.parse(raw) as Partial<ScreenshotSettings> & { version?: number }
if (parsed.version !== SETTINGS_VERSION) {
// 结构变更:重置为新默认值(应用新的默认快捷键 / 贴图快捷键 / 保留数量)
settings.value = { ...DEFAULT_SETTINGS }
saveSettings()
} else {
settings.value = { ...DEFAULT_SETTINGS, ...parsed } settings.value = { ...DEFAULT_SETTINGS, ...parsed }
} }
}
} catch (e) { } catch (e) {
console.error('[screenshot] 加载设置失败', e) console.error('[screenshot] 加载设置失败', e)
localStorage.removeItem(SETTINGS_KEY) localStorage.removeItem(SETTINGS_KEY)
@@ -74,7 +106,7 @@ export const useScreenshotStore = defineStore('screenshot', () => {
function saveSettings() { function saveSettings() {
try { try {
localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings.value)) localStorage.setItem(SETTINGS_KEY, JSON.stringify({ version: SETTINGS_VERSION, ...settings.value }))
} catch (e) { } catch (e) {
console.error('[screenshot] 保存设置失败', e) console.error('[screenshot] 保存设置失败', e)
} }
@@ -83,6 +115,11 @@ export const useScreenshotStore = defineStore('screenshot', () => {
function setSettings(patch: Partial<ScreenshotSettings>) { function setSettings(patch: Partial<ScreenshotSettings>) {
settings.value = { ...settings.value, ...patch } settings.value = { ...settings.value, ...patch }
saveSettings() saveSettings()
// 保留数量变更:立即裁剪历史(含删除超限缓存文件)
if (patch.historyLimit !== undefined) {
enforceHistoryLimit()
persistHistory()
}
} }
// ===== 窗口定位 ===== // ===== 窗口定位 =====
@@ -193,7 +230,7 @@ export const useScreenshotStore = defineStore('screenshot', () => {
if (readyListenerInit) return if (readyListenerInit) return
readyListenerInit = true readyListenerInit = true
resetOverlayReady() resetOverlayReady()
await listen(EVENTS.screenshotOverlayReady, () => { overlayReadyUnlisten = await listen(EVENTS.screenshotOverlayReady, () => {
overlayReadyResolve?.() overlayReadyResolve?.()
}) })
await ensureOverlay() await ensureOverlay()
@@ -247,9 +284,8 @@ export const useScreenshotStore = defineStore('screenshot', () => {
}) })
} }
/** 追加历史项:删除超出上限的最旧缓存文件 */ /** 按保留数量裁剪历史(超出部分删除缓存文件 */
function addRecent(thumb: string, filePath: string, width: number, height: number, mode: string) { function enforceHistoryLimit() {
recent.value.unshift({ id: crypto.randomUUID(), thumb, filePath, width, height, time: Date.now(), mode })
const limit = Math.max(1, settings.value.historyLimit) const limit = Math.max(1, settings.value.historyLimit)
if (recent.value.length > limit) { if (recent.value.length > limit) {
const removed = recent.value.splice(limit) const removed = recent.value.splice(limit)
@@ -259,10 +295,68 @@ export const useScreenshotStore = defineStore('screenshot', () => {
} }
} }
/** 持久化历史元数据(含缩略图与定位坐标)到 localStorage,供重启恢复 */
function persistHistory() {
try {
localStorage.setItem(
HISTORY_KEY,
JSON.stringify({ version: HISTORY_VERSION, items: recent.value }),
)
} catch (e) {
console.error('[screenshot] 持久化历史失败(可能超出存储限额)', e)
}
}
/** 应用启动时恢复历史:版本不匹配/损坏则丢弃;并按当前保留数量清理超限文件 */
function loadHistory() {
try {
const raw = localStorage.getItem(HISTORY_KEY)
if (!raw) return
const parsed = JSON.parse(raw) as { version?: number; items?: RecentCapture[] }
if (parsed.version !== HISTORY_VERSION || !Array.isArray(parsed.items)) {
localStorage.removeItem(HISTORY_KEY)
return
}
const limit = Math.max(1, settings.value.historyLimit)
const items = parsed.items.slice(0, limit)
for (const extra of parsed.items.slice(limit)) {
void commands.screenshotDeleteCache(extra.filePath)
}
recent.value = items
} catch (e) {
console.error('[screenshot] 恢复历史失败', e)
recent.value = []
}
}
/** 追加历史项:持久化 + 删除超出上限的最旧缓存文件 */
function addRecent(
thumb: string,
filePath: string,
width: number,
height: number,
mode: string,
pos?: { x: number; y: number },
) {
recent.value.unshift({
id: crypto.randomUUID(),
thumb,
filePath,
width,
height,
time: Date.now(),
mode,
...(pos ? { posX: pos.x, posY: pos.y } : {}),
})
enforceHistoryLimit()
persistHistory()
}
/** 移除单个历史项(同时删除缓存文件) */ /** 移除单个历史项(同时删除缓存文件) */
async function removeRecent(item: RecentCapture) { async function removeRecent(item: RecentCapture) {
const idx = recent.value.findIndex((r) => r.id === item.id) const idx = recent.value.findIndex((r) => r.id === item.id)
if (idx >= 0) recent.value.splice(idx, 1) if (idx >= 0) recent.value.splice(idx, 1)
persistHistory()
try { try {
await commands.screenshotDeleteCache(item.filePath) await commands.screenshotDeleteCache(item.filePath)
} catch (e) { } catch (e) {
@@ -275,6 +369,7 @@ export const useScreenshotStore = defineStore('screenshot', () => {
void commands.screenshotDeleteCache(item.filePath) void commands.screenshotDeleteCache(item.filePath)
} }
recent.value = [] recent.value = []
persistHistory()
} }
/** 从历史缓存加载完整 PNG base64(一次性,不常驻) */ /** 从历史缓存加载完整 PNG base64(一次性,不常驻) */
@@ -303,13 +398,27 @@ export const useScreenshotStore = defineStore('screenshot', () => {
} }
/** 覆盖层/编辑器导出处理:记录历史(缩略图 + 缓存落盘)+ 按设置自动保存 */ /** 覆盖层/编辑器导出处理:记录历史(缩略图 + 缓存落盘)+ 按设置自动保存 */
async function handleExport(payload: { pngBase64: string; width: number; height: number }) { async function handleExport(payload: {
const { pngBase64, width, height } = payload pngBase64: string
width: number
height: number
/** 截图时框选区域的物理屏幕坐标(贴图窗口按原位置显示;编辑器导出缺失) */
posX?: number
posY?: number
}) {
const { pngBase64, width, height, posX, posY } = payload
try { try {
// 完整图落盘缓存目录,历史只保留缩略图,避免完整 base64 常驻内存 // 完整图落盘缓存目录,历史只保留缩略图,避免完整 base64 常驻内存
const filePath = await commands.screenshotSaveCache(pngBase64) const filePath = await commands.screenshotSaveCache(pngBase64)
const thumb = await makeThumb(pngBase64, width, height) const thumb = await makeThumb(pngBase64, width, height)
addRecent(thumb, filePath, width, height, 'capture') addRecent(
thumb,
filePath,
width,
height,
'capture',
posX !== undefined && posY !== undefined ? { x: posX, y: posY } : undefined,
)
} catch (e) { } catch (e) {
console.error('[screenshot] 历史缓存失败', e) console.error('[screenshot] 历史缓存失败', e)
} }
@@ -335,7 +444,9 @@ export const useScreenshotStore = defineStore('screenshot', () => {
pngBase64: string pngBase64: string
width: number width: number
height: number height: number
}>('screenshot-exported', (e) => { posX?: number
posY?: number
}>(EVENTS.screenshotExported, (e) => {
void handleExport(e.payload) void handleExport(e.payload)
}) })
} }
@@ -371,11 +482,132 @@ export const useScreenshotStore = defineStore('screenshot', () => {
} }
} }
// ===== 贴图窗口 =====
function resetPinReady() {
pinReadyPromise = new Promise<void>((resolve) => {
pinReadyResolve = resolve
})
}
/** 等待贴图窗口就绪(首次创建时等 onMounted 完成,之后立即返回) */
async function waitPinReady() {
await Promise.race([
pinReadyPromise,
new Promise<void>((r) => setTimeout(r, 3000)),
])
}
/** 注册贴图窗口就绪监听(首次贴图前调用) */
async function ensurePinReadyListener() {
if (pinReadyListenerInit) return
pinReadyListenerInit = true
resetPinReady()
pinReadyUnlisten = await listen(EVENTS.screenshotPinReady, () => {
pinReadyResolve?.()
})
}
/** 创建常驻贴图窗口(无边框、透明、置顶、无任务栏;首次贴图时创建,之后复用) */
async function ensurePinWindow() {
if (pinWin) {
const existing = await WebviewWindow.getByLabel(PIN_LABEL).catch(() => null)
if (existing) {
pinWin = existing
return
}
pinWin = null
}
resetPinReady()
pinWin = new WebviewWindow(PIN_LABEL, {
url: 'index.html#screenshot-pin',
title: '贴图',
x: 0,
y: 0,
width: 120,
height: 120,
// 创建时必须 resizable:trueWindows 上非 resizable 窗口 setSize 会失效);
// 每次布局结束后在贴图窗口内恢复 setResizable(false),禁用边缘拖动缩放,仅保留左键自由拖动
resizable: true,
decorations: false,
alwaysOnTop: true,
skipTaskbar: true,
transparent: true,
shadow: false,
focus: false,
visible: false,
})
await waitPinReady()
}
/**
* index
* index
*/
async function togglePin(index?: number) {
if (!recent.value.length) {
toast('暂无截图可贴图')
return
}
await ensurePinReadyListener()
if (pinWin) {
try {
if (await pinWin.isVisible()) {
await pinWin.hide()
return
}
} catch {
/* 窗口可能尚未就绪,忽略 */
}
}
const i = Math.max(0, Math.min(index ?? 0, recent.value.length - 1))
localStorage.setItem(PIN_INDEX_KEY, String(i))
await ensurePinWindow()
await emit(EVENTS.screenshotPinShow)
}
/** 监听 Rust 侧 emit 的 'screenshot-pin-shortcut' 事件(贴图快捷键按下时切换) */
async function initPinShortcutListener() {
if (pinShortcutUnlisten) return
pinShortcutUnlisten = await listen(EVENTS.screenshotPinShortcut, () => {
void togglePin()
})
}
/** 应用启动时按已保存的设置注册贴图全局快捷键(默认 Alt+1) */
async function initPinShortcutRegistration() {
try {
await commands.screenshotRegisterPinShortcut(settings.value.pinShortcut)
} catch (e) {
console.error('[screenshot] 贴图快捷键注册失败', e)
}
}
/** 修改贴图快捷键:持久化 + 重新注册(传空字符串禁用) */
async function setPinShortcut(shortcut: string) {
const next = shortcut.trim()
setSettings({ pinShortcut: next })
try {
await commands.screenshotRegisterPinShortcut(next)
return true
} catch (e) {
console.error('[screenshot] 贴图快捷键注册失败', e)
return false
}
}
function destroyExportListener() { function destroyExportListener() {
if (exportUnlisten) { if (exportUnlisten) {
exportUnlisten() exportUnlisten()
exportUnlisten = null exportUnlisten = null
} }
if (overlayReadyUnlisten) {
overlayReadyUnlisten()
overlayReadyUnlisten = null
}
if (pinReadyUnlisten) {
pinReadyUnlisten()
pinReadyUnlisten = null
}
} }
function destroyShortcutListener() { function destroyShortcutListener() {
@@ -383,6 +615,10 @@ export const useScreenshotStore = defineStore('screenshot', () => {
shortcutUnlisten() shortcutUnlisten()
shortcutUnlisten = null shortcutUnlisten = null
} }
if (pinShortcutUnlisten) {
pinShortcutUnlisten()
pinShortcutUnlisten = null
}
} }
return { return {
@@ -400,12 +636,17 @@ export const useScreenshotStore = defineStore('screenshot', () => {
removeRecent, removeRecent,
loadFullImage, loadFullImage,
loadSettings, loadSettings,
loadHistory,
setSettings, setSettings,
setShortcut, setShortcut,
togglePin,
setPinShortcut,
initExportListener, initExportListener,
destroyExportListener, destroyExportListener,
initShortcutListener, initShortcutListener,
initShortcutRegistration, initShortcutRegistration,
destroyShortcutListener, destroyShortcutListener,
initPinShortcutListener,
initPinShortcutRegistration,
} }
}) })
+17
View File
@@ -23,6 +23,23 @@ export default defineConfig(async () => ({
input: { input: {
main: fileURLToPath(new URL("./index.html", import.meta.url)), main: fileURLToPath(new URL("./index.html", import.meta.url)),
}, },
// 分包策略:vendor 独立 chunk,业务迭代时不强制重下载依赖
// - vue/pinia/@vueuse: Vue 生态核心,变更频率低
// - reka-ui/@lucide: UI 组件库,体积较大且稳定
// - tauri: Tauri 前端 API,仅在 Tauri 升级时变化
output: {
manualChunks: {
'vue-vendor': ['vue', 'pinia', '@vueuse/core'],
'ui-vendor': ['reka-ui', '@lucide/vue'],
'tauri-vendor': [
'@tauri-apps/api',
'@tauri-apps/plugin-autostart',
'@tauri-apps/plugin-dialog',
'@tauri-apps/plugin-global-shortcut',
'@tauri-apps/plugin-opener',
],
},
},
}, },
}, },